[
  {
    "path": ".github/CODEOWNERS",
    "content": "* @gofiber/maintainers"
  },
  {
    "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        - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord).\n        - If you think Fiber contrib don't have a nice feature that you think, open the issue with **✏️ 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 as clear and detailed.\"\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 happens.\"\n      placeholder: \"Tell us what contrib should normally do.\"\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: \"Contrib package Version\"\n      description: \"Some bugs may be fixed in future contrib releases, so we have to know your contrib package version.\"\n      placeholder: \"Write your contrib version. (v1.0.0, v1.1.0...)\"\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 you think related to the issue.\"\n      render: go\n      value: |\n        package main\n\n        // Replace <MAJOR> with the module's current major version (omit the `/v<MAJOR>` suffix entirely for v1 modules).\n        import \"github.com/gofiber/contrib/v3/%package%\"\n\n        func main() {\n          // Steps to reproduce\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/master/.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/feature-request.yaml",
    "content": "name: \"\\U0001F680 Feature Request\"\ntitle: \"\\U0001F680 [Feature]: \"\ndescription: Suggest an idea to improve this project.\nlabels: [\"✏️ Feature\"]\n\nbody:\n  - type: markdown\n    id: notice\n    attributes:\n      value: |\n        ### Notice\n        - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord).\n        - If you think this is just a bug, open the issue with **☢️ Bug Report** template.\n        - Write your issue with clear and understandable English.\n  - type: textarea\n    id: description\n    attributes:\n      label: \"Feature Description\"\n      description: \"A clear and detailed description of the feature we need to do.\"\n      placeholder: \"Explain your feature as clear and detailed.\"\n    validations:\n      required: true\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: \"Additional Context (optional)\"\n      description: \"If you have something else to describe, write them here.\"\n      placeholder: \"Write here what you can describe differently.\"\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 to explain the feature better.\"\n      render: go\n      value: |\n        package main\n\n        // Replace <MAJOR> with the module's current major version (omit the `/v<MAJOR>` suffix entirely for v1 modules).\n        import \"github.com/gofiber/contrib/v3/%package%\"\n\n        func main() {\n          // Steps to reproduce\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/master/.github/CODE_OF_CONDUCT.md).\"\n          required: true\n        - label: \"I have checked for existing issues that describe my suggestion prior to opening this one.\"\n          required: true\n        - label: \"I understand that improperly formatted feature requests may be closed without explanation.\"\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        - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord).\n        - If you think this is just a bug, open the issue with **☢️ Bug Report** template.\n        - If you think Fiber contrib don't have a nice feature that you think, open the issue with **✏️ 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 as clear and detailed.\"\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 to explain the feature better.\"\n      render: go\n      value: |\n        package main\n\n        // Replace <MAJOR> with the module's current major version (omit the `/v<MAJOR>` suffix entirely for v1 modules).\n        import \"github.com/gofiber/contrib/v3/%package%\"\n\n        func main() {\n          // Steps to reproduce\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/master/.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/dependabot.yml",
    "content": "# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#directories\n\nversion: 2\nupdates:\n    - package-ecosystem: \"github-actions\"\n      open-pull-requests-limit: 50\n      directory: \"/\"\n      labels:\n          - \"🤖 Dependencies\"\n      schedule:\n          interval: \"daily\"\n\n    # gomod split by alphabet ranges to reduce group PR size\n    # a-l ~8 modules | m-r ~6 modules | s-z ~8 modules\n    - package-ecosystem: \"gomod\"\n      open-pull-requests-limit: 20\n      allow:\n          - dependency-type: \"all\"\n      directories:\n          - \"/v3/a*\"\n          - \"/v3/b*\"\n          - \"/v3/c*\"\n          - \"/v3/d*\"\n          - \"/v3/e*\"\n          - \"/v3/f*\"\n          - \"/v3/g*\"\n          - \"/v3/h*\"\n          - \"/v3/i*\"\n          - \"/v3/j*\"\n          - \"/v3/k*\"\n          - \"/v3/l*\"\n      labels:\n          - \"🤖 Dependencies\"\n      schedule:\n          interval: \"daily\"\n      groups:\n          fiber-modules:\n              patterns:\n                  - \"github.com/gofiber/fiber/**\"\n          fiber-utils-modules:\n              patterns:\n                  - \"github.com/gofiber/utils\"\n                  - \"github.com/gofiber/utils/**\"\n          fiber-schema-modules:\n              patterns:\n                  - \"github.com/gofiber/schema\"\n                  - \"github.com/gofiber/schema/**\"\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          opentelemetry-modules:\n              patterns:\n                  - \"go.opentelemetry.io/**\"\n          fasthttp-websocket-modules:\n              patterns:\n                  - \"github.com/fasthttp/websocket/**\"\n          valyala-utils-modules:\n              patterns:\n                  - \"github.com/valyala/bytebufferpool\"\n                  - \"github.com/valyala/tcplisten\"\n          mattn-modules:\n              patterns:\n                  - \"github.com/mattn/go-colorable\"\n                  - \"github.com/mattn/go-isatty\"\n                  - \"github.com/mattn/go-runewidth\"\n          shirou-modules:\n              patterns:\n                  - \"github.com/shirou/gopsutil\"\n                  - \"github.com/shirou/gopsutil/**\"\n          ebitengine-modules:\n              patterns:\n                  - \"github.com/ebitengine/**\"\n          klauspost-modules:\n              patterns:\n                  - \"github.com/klauspost/**\"\n          tklauser-modules:\n              patterns:\n                  - \"github.com/tklauser/**\"\n          google-modules:\n              patterns:\n                  - \"github.com/google/**\"\n                  - \"google.golang.org/**\"\n          testing-modules:\n              patterns:\n                  - \"github.com/stretchr/**\"\n                  - \"github.com/davecgh/go-spew\"\n                  - \"github.com/pmezard/go-difflib\"\n          check-testing-modules:\n              patterns:\n                  - \"gopkg.in/check.v*\"\n                  - \"github.com/kr/**\"\n                  - \"github.com/rogpeppe/go-internal\"\n          tinylib-modules:\n              patterns:\n                  - \"github.com/tinylib/**\"\n          msgp-modules:\n              patterns:\n                  - \"github.com/philhofer/fwd\"\n          openapi-modules:\n              patterns:\n                  - \"github.com/go-openapi/**\"\n          yaml-modules:\n              patterns:\n                  - \"gopkg.in/yaml.*\"\n                  - \"go.yaml.in/yaml/**\"\n                  - \"sigs.k8s.io/yaml\"\n          andybalholm-modules:\n              patterns:\n                  - \"github.com/andybalholm/**\"\n\n    - package-ecosystem: \"gomod\"\n      open-pull-requests-limit: 20\n      allow:\n          - dependency-type: \"all\"\n      directories:\n          - \"/v3/m*\"\n          - \"/v3/n*\"\n          - \"/v3/o*\"\n          - \"/v3/o*/*\"\n          - \"/v3/p*\"\n          - \"/v3/q*\"\n          - \"/v3/r*\"\n      labels:\n          - \"🤖 Dependencies\"\n      schedule:\n          interval: \"daily\"\n      groups:\n          fiber-modules:\n              patterns:\n                  - \"github.com/gofiber/fiber/**\"\n          fiber-utils-modules:\n              patterns:\n                  - \"github.com/gofiber/utils\"\n                  - \"github.com/gofiber/utils/**\"\n          fiber-schema-modules:\n              patterns:\n                  - \"github.com/gofiber/schema\"\n                  - \"github.com/gofiber/schema/**\"\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          opentelemetry-modules:\n              patterns:\n                  - \"go.opentelemetry.io/**\"\n          fasthttp-websocket-modules:\n              patterns:\n                  - \"github.com/fasthttp/websocket/**\"\n          valyala-utils-modules:\n              patterns:\n                  - \"github.com/valyala/bytebufferpool\"\n                  - \"github.com/valyala/tcplisten\"\n          mattn-modules:\n              patterns:\n                  - \"github.com/mattn/go-colorable\"\n                  - \"github.com/mattn/go-isatty\"\n                  - \"github.com/mattn/go-runewidth\"\n          shirou-modules:\n              patterns:\n                  - \"github.com/shirou/gopsutil\"\n                  - \"github.com/shirou/gopsutil/**\"\n          ebitengine-modules:\n              patterns:\n                  - \"github.com/ebitengine/**\"\n          klauspost-modules:\n              patterns:\n                  - \"github.com/klauspost/**\"\n          tklauser-modules:\n              patterns:\n                  - \"github.com/tklauser/**\"\n          google-modules:\n              patterns:\n                  - \"github.com/google/**\"\n                  - \"google.golang.org/**\"\n          testing-modules:\n              patterns:\n                  - \"github.com/stretchr/**\"\n                  - \"github.com/davecgh/go-spew\"\n                  - \"github.com/pmezard/go-difflib\"\n          check-testing-modules:\n              patterns:\n                  - \"gopkg.in/check.v*\"\n                  - \"github.com/kr/**\"\n                  - \"github.com/rogpeppe/go-internal\"\n          tinylib-modules:\n              patterns:\n                  - \"github.com/tinylib/**\"\n          msgp-modules:\n              patterns:\n                  - \"github.com/philhofer/fwd\"\n          openapi-modules:\n              patterns:\n                  - \"github.com/go-openapi/**\"\n          yaml-modules:\n              patterns:\n                  - \"gopkg.in/yaml.*\"\n                  - \"go.yaml.in/yaml/**\"\n                  - \"sigs.k8s.io/yaml\"\n          andybalholm-modules:\n              patterns:\n                  - \"github.com/andybalholm/**\"\n\n    - package-ecosystem: \"gomod\"\n      open-pull-requests-limit: 20\n      allow:\n          - dependency-type: \"all\"\n      directories:\n          - \"/v3/s*\"\n          - \"/v3/t*\"\n          - \"/v3/u*\"\n          - \"/v3/v*\"\n          - \"/v3/w*\"\n          - \"/v3/x*\"\n          - \"/v3/y*\"\n          - \"/v3/z*\"\n      labels:\n          - \"🤖 Dependencies\"\n      schedule:\n          interval: \"daily\"\n      groups:\n          fiber-modules:\n              patterns:\n                  - \"github.com/gofiber/fiber/**\"\n          fiber-utils-modules:\n              patterns:\n                  - \"github.com/gofiber/utils\"\n                  - \"github.com/gofiber/utils/**\"\n          fiber-schema-modules:\n              patterns:\n                  - \"github.com/gofiber/schema\"\n                  - \"github.com/gofiber/schema/**\"\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          opentelemetry-modules:\n              patterns:\n                  - \"go.opentelemetry.io/**\"\n          fasthttp-websocket-modules:\n              patterns:\n                  - \"github.com/fasthttp/websocket/**\"\n          valyala-utils-modules:\n              patterns:\n                  - \"github.com/valyala/bytebufferpool\"\n                  - \"github.com/valyala/tcplisten\"\n          mattn-modules:\n              patterns:\n                  - \"github.com/mattn/go-colorable\"\n                  - \"github.com/mattn/go-isatty\"\n                  - \"github.com/mattn/go-runewidth\"\n          shirou-modules:\n              patterns:\n                  - \"github.com/shirou/gopsutil\"\n                  - \"github.com/shirou/gopsutil/**\"\n          ebitengine-modules:\n              patterns:\n                  - \"github.com/ebitengine/**\"\n          klauspost-modules:\n              patterns:\n                  - \"github.com/klauspost/**\"\n          tklauser-modules:\n              patterns:\n                  - \"github.com/tklauser/**\"\n          google-modules:\n              patterns:\n                  - \"github.com/google/**\"\n                  - \"google.golang.org/**\"\n          testing-modules:\n              patterns:\n                  - \"github.com/stretchr/**\"\n                  - \"github.com/davecgh/go-spew\"\n                  - \"github.com/pmezard/go-difflib\"\n          check-testing-modules:\n              patterns:\n                  - \"gopkg.in/check.v*\"\n                  - \"github.com/kr/**\"\n                  - \"github.com/rogpeppe/go-internal\"\n          tinylib-modules:\n              patterns:\n                  - \"github.com/tinylib/**\"\n          msgp-modules:\n              patterns:\n                  - \"github.com/philhofer/fwd\"\n          openapi-modules:\n              patterns:\n                  - \"github.com/go-openapi/**\"\n          yaml-modules:\n              patterns:\n                  - \"gopkg.in/yaml.*\"\n                  - \"go.yaml.in/yaml/**\"\n                  - \"sigs.k8s.io/yaml\"\n          andybalholm-modules:\n              patterns:\n                  - \"github.com/andybalholm/**\"\n"
  },
  {
    "path": ".github/release-plan.yml",
    "content": "# Release plan for gofiber/contrib.\n#\n# Only modules with ordering constraints need explicit waves.\n# The final wave with `auto-discover: true` picks up all remaining\n# draft releases that weren't handled by earlier waves.\n#\n# Constraint: socketio depends on websocket (local replace).\n# Websocket's post-release hook triggers dependabot in this repo\n# immediately so socketio picks up the new version before wave 2.\n#\n# After all modules are published, 'after-release' is dispatched once.\n# The after-release.yml workflow defines which repos to notify.\n\nwave-delay-minutes: 30\n\nwaves:\n  - name: base\n    modules:\n      - name: websocket\n        tag-prefix: \"v3/websocket/\"\n        post-release:\n          - action: dispatch\n            repo: gofiber/contrib\n            event: dependabot-on-demand\n\n  - name: remaining\n    auto-discover: true\n\npost-release:\n  - action: dispatch\n    event: after-release\n  - action: dispatch\n    event: sync-docs\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/parallel-go-test.sh",
    "content": "#!/usr/bin/env bash\n# parallel-go-test.sh (refreshed)\n# Recursively find go.mod files under the current directory and run tests in each module.\n# - truncates tests-errors.log at start\n# - runs tests in parallel (jobs = CPU cores)\n# - prefers `gotestsum` when available; otherwise uses `go test`\n# - on failures, appends annotated error blocks (source line + one-line context) to tests-errors.log immediately\n# - filters out successful test noise (=== RUN / --- PASS / PASS / ok ...)\n# - atomic appends using mkdir-lock so parallel jobs don't interleave\n# - colored terminal output (OK = green, FAIL = red, WARN = yellow)\n# - handles Ctrl+C / SIGTERM: kills children and cleans up\n# - option: -m to run `go mod download` and `go mod vendor` in each module before tests\n\nset -euo pipefail\nIFS=$'\\n\\t'\n\nJOBS=\"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)\"\nLOGFILE=\"tests-errors.log\"\nRUN_GOMOD=0\n\nusage() {\n  cat <<'USAGE'\nUsage: ./parallel-go-test.sh [-j JOBS] [-o LOGFILE] [-m]\n\nOptions:\n  -j JOBS     Number of parallel jobs (defaults to CPU cores)\n  -o LOGFILE  Path to central logfile (defaults to tests-errors.log)\n  -m          Run `go mod download` and `go mod vendor` in each module before running tests\n\nExamples:\n  ./parallel-go-test.sh            # default behaviour\n  ./parallel-go-test.sh -m         # run `go mod download` + `go mod vendor` first\n  ./parallel-go-test.sh -j 8 -m    # 8 parallel jobs and run mod step\nUSAGE\n}\n\n# parse options\nwhile getopts \":j:o:mh\" opt; do\n  case \"$opt\" in\n    j) JOBS=\"$OPTARG\" ;;\n    o) LOGFILE=\"$OPTARG\" ;;\n    m) RUN_GOMOD=1 ;;\n    h) usage; exit 0 ;;\n    \\?) printf 'Unknown option: -%s\\n' \"$OPTARG\"; usage; exit 2 ;;\n    :) printf 'Option -%s requires an argument.\\n' \"$OPTARG\"; usage; exit 2 ;;\n  esac\ndone\nshift $((OPTIND -1))\n\n# Colors (only if stdout is a tty)\nif [ -t 1 ]; then\n  GREEN=$'\\e[32m'\n  RED=$'\\e[31m'\n  YELLOW=$'\\e[33m'\n  RESET=$'\\e[0m'\nelse\n  GREEN=\"\"\n  RED=\"\"\n  YELLOW=\"\"\n  RESET=\"\"\nfi\n\n# truncate central logfile at start\n: > \"$LOGFILE\"\n\n# detect gotestsum\nUSE_GOTESTSUM=0\nif command -v gotestsum >/dev/null 2>&1; then\n  USE_GOTESTSUM=1\nfi\n\n# find all module directories (skip common vendor trees)\nmapfile -t MODULE_DIRS < <(\n  find . \\( -path \"./.git\" -o -path \"./vendor\" -o -path \"./node_modules\" \\) -prune -o \\\n    -type f -name 'go.mod' -print0 |\n  xargs -0 -n1 dirname |\n  sort -u\n)\n\nif [ \"${#MODULE_DIRS[@]}\" -eq 0 ]; then\n  printf '%s\\n' \"No go.mod files found. Nothing to do.\"\n  exit 0\nfi\n\n# create absolute temp dir\nTMPDIR=\"$(mktemp -d 2>/dev/null || mktemp -d /tmp/tests-logs.XXXXXX)\"\nif [ -z \"$TMPDIR\" ] || [ ! -d \"$TMPDIR\" ]; then\n  printf '%s\\n' \"Failed to create temporary directory\" >&2\n  exit 2\nfi\n\n# cleanup routine\ncleanup_tmpdir() {\n  local attempts=0\n  while [ \"$attempts\" -lt 5 ]; do\n    if rm -rf \"$TMPDIR\" 2>/dev/null; then\n      break\n    fi\n    attempts=$((attempts+1))\n    sleep 0.1\n  done\n  if [ -d \"$TMPDIR\" ]; then\n    printf '%s\\n' \"WARNING: could not fully remove temporary dir: $TMPDIR\"\n  fi\n}\n\n# signal handling: kill children, wait, cleanup, exit\npids=()\non_interrupt() {\n  printf '%b\\n' \"${YELLOW}Received interrupt. Killing background jobs...${RESET}\"\n  for pid in \"${pids[@]:-}\"; do\n    if kill -0 \"$pid\" 2>/dev/null; then\n      kill \"$pid\" 2>/dev/null || true\n    fi\n  done\n  sleep 0.1\n  for pid in \"${pids[@]:-}\"; do\n    if kill -0 \"$pid\" 2>/dev/null; then\n      kill -9 \"$pid\" 2>/dev/null || true\n    fi\n  done\n  cleanup_tmpdir\n  printf '%b\\n' \"${YELLOW}Aborted by user.${RESET}\"\n  exit 130\n}\ntrap on_interrupt INT TERM\n\n# helper: sanitize a dir to a filename-safe token\nsanitize_name() {\n  local d=\"$1\"\n  d=\"${d#./}\"\n  d=\"${d//\\//__}\"\n  d=\"${d// /_}\"\n  printf '%s' \"${d//[^A-Za-z0-9._-]/_}\"\n}\n\n# annotate a module's temp log and append to central logfile atomically\n# This version: only appends when failure indicators are present and strips PASS/RUN noise\nannotate_and_append() {\n  local src_log=\"$1\"\n  local module_dir=\"$2\"\n  local lockdir=\"$TMPDIR/.lock\"\n\n  # quick check: only append logs that contain failure indicators\n  if ! grep -E -q '(--- FAIL:|^FAIL\\b|panic:|exit status|FAIL\\t|FAIL:)' \"$src_log\"; then\n    # nothing to do (only PASS/ok output)\n    rm -f \"$src_log\" >/dev/null 2>&1 || true\n    return\n  fi\n\n  local annotated\n  annotated=\"$(mktemp \"$TMPDIR/annotated.XXXXXX\")\" || {\n    until mkdir \"$lockdir\" 2>/dev/null; do sleep 0.01; done\n    printf '==== %s ====\\n' \"$module_dir\" >> \"$LOGFILE\"\n    # filter out PASS/OK noise when falling back\n    grep -v -E '^(=== RUN|--- PASS:|^PASS$|^ok\\s)' \"$src_log\" >> \"$LOGFILE\" || true\n    rm -f \"$src_log\" || true\n    rmdir \"$lockdir\" 2>/dev/null || true\n    return\n  }\n\n  # process lines in src_log, skipping PASS/RUN lines and annotating file:line occurrences\n  while IFS= read -r line || [ -n \"$line\" ]; do\n    # skip successful-test noise\n    if [[ $line =~ ^(===\\ RUN|---\\ PASS:|^PASS$|^ok\\s) ]]; then\n      continue\n    fi\n\n    # match paths like path/to/file.go:LINE or file.go:LINE:COL\n    if [[ $line =~ ^([^:]+\\.go):([0-9]+):?([0-9]*)[:[:space:]]*(.*)$ ]]; then\n      local fp=\"${BASH_REMATCH[1]}\"\n      local ln=\"${BASH_REMATCH[2]}\"\n      local col=\"${BASH_REMATCH[3]}\"\n      local rest=\"${BASH_REMATCH[4]}\"\n      local candidate=\"\"\n\n      if [ -f \"$fp\" ]; then\n        candidate=\"$fp\"\n      elif [ -f \"$module_dir/$fp\" ]; then\n        candidate=\"$module_dir/$fp\"\n      elif [ -f \"./$fp\" ]; then\n        candidate=\"./$fp\"\n      fi\n\n      if [ -n \"$candidate\" ]; then\n        local start=$(( ln > 1 ? ln - 1 : 1 ))\n        local end=$(( ln + 1 ))\n        printf '%s\\n' \"---- source: $candidate:$ln ----\" >> \"$annotated\"\n        awk -v s=\"$start\" -v e=\"$end\" 'NR>=s && NR<=e { printf(\"%6d  %s\\n\", NR, $0) }' \"$candidate\" >> \"$annotated\"\n        printf '%s\\n\\n' \"Error: $line\" >> \"$annotated\"\n      else\n        printf '%s\\n' \"---- (source not found) $line ----\" >> \"$annotated\"\n      fi\n    else\n      printf '%s\\n' \"$line\" >> \"$annotated\"\n    fi\n  done < \"$src_log\"\n\n  # append atomically under lock\n  until mkdir \"$lockdir\" 2>/dev/null; do\n    sleep 0.01\n  done\n  {\n    printf '==== %s ====\\n' \"$module_dir\"\n    cat \"$annotated\"\n    printf '\\n\\n'\n  } >> \"$LOGFILE\"\n  rm -f \"$annotated\" \"$src_log\" || true\n  rmdir \"$lockdir\" 2>/dev/null || true\n}\n\n# run tests for one module (with optional go mod download + vendor step)\nrun_tests() {\n  local module_dir=\"$1\"\n  local safe\n  safe=\"$(sanitize_name \"$module_dir\")\"\n  local mod_log=\"$TMPDIR/$safe.log\"\n  mkdir -p \"$(dirname \"$mod_log\")\"\n\n  # optionally run `go mod download` and `go mod vendor` first\n  if [ \"$RUN_GOMOD\" -eq 1 ]; then\n    local mod_step_log=\"$TMPDIR/$safe.modlog\"\n    : > \"$mod_step_log\"\n    ( cd \"$module_dir\" 2>/dev/null && go mod download >>\"$mod_step_log\" 2>&1 ) || true\n    ( cd \"$module_dir\" 2>/dev/null && go mod vendor >>\"$mod_step_log\" 2>&1 ) || true\n    if [ -s \"$mod_step_log\" ]; then\n      printf '%b\\n' \"${YELLOW}MOD:  ${RESET}$module_dir (go mod output appended)\"\n      annotate_and_append \"$mod_step_log\" \"$module_dir\"\n    else\n      rm -f \"$mod_step_log\" >/dev/null 2>&1 || true\n    fi\n  fi\n\n  # run tests: always capture stdout+stderr to per-module log so nothing prints on success\n  if [ \"$USE_GOTESTSUM\" -eq 1 ]; then\n    # use gotestsum but still capture its output to file\n    if ( cd \"$module_dir\" 2>/dev/null && gotestsum --format standard-verbose -- -test.v ./... >\"$mod_log\" 2>&1 ); then\n      printf '%b\\n' \"${GREEN}OK:   ${RESET}$module_dir\"\n      rm -f \"$mod_log\" >/dev/null 2>&1 || true\n      return 0\n    else\n      printf '%b\\n' \"${RED}FAIL: ${RESET}$module_dir (appending to $LOGFILE)\"\n      annotate_and_append \"$mod_log\" \"$module_dir\"\n      return 1\n    fi\n  else\n    # fallback to go test; capture everything\n    if ( cd \"$module_dir\" 2>/dev/null && go test ./... -run \"\" -v >\"$mod_log\" 2>&1 ); then\n      printf '%b\\n' \"${GREEN}OK:   ${RESET}$module_dir\"\n      rm -f \"$mod_log\" >/dev/null 2>&1 || true\n      return 0\n    else\n      printf '%b\\n' \"${RED}FAIL: ${RESET}$module_dir (appending to $LOGFILE)\"\n      annotate_and_append \"$mod_log\" \"$module_dir\"\n      return 1\n    fi\n  fi\n}\n\n# main launcher: spawn jobs, throttle to JOBS, wait properly\nfail_count=0\n\nfor md in \"${MODULE_DIRS[@]}\"; do\n  run_tests \"$md\" &\n  pids+=( \"$!\" )\n\n  if [ \"${#pids[@]}\" -ge \"$JOBS\" ]; then\n    if wait \"${pids[0]}\"; then\n      :\n    else\n      fail_count=$((fail_count+1))\n    fi\n    pids=( \"${pids[@]:1}\" )\n  fi\ndone\n\n# wait remaining background jobs\nfor pid in \"${pids[@]:-}\"; do\n  if wait \"$pid\"; then :; else fail_count=$((fail_count+1)); fi\ndone\n\n# final cleanup and status\ncleanup_tmpdir\n\nif [ \"$fail_count\" -gt 0 ]; then\n  printf '\\n%b\\n' \"${RED}Done. $fail_count module(s) failed. See $LOGFILE for details (annotated snippets included).${RESET}\"\n  exit 1\nelse\n  printf '\\n%b\\n' \"${GREEN}Done. All modules tested successfully.${RESET}\"\n  if [ -f \"$LOGFILE\" ] && [ ! -s \"$LOGFILE\" ]; then\n    rm -f \"$LOGFILE\"\n  fi\n  exit 0\nfi\n"
  },
  {
    "path": ".github/workflows/after-release.yml",
    "content": "name: After Release\n\non:\n  release:\n    types: [published]\n  repository_dispatch:\n    types: [after-release]\n  workflow_dispatch:\n\njobs:\n  # Websocket releases update socketio dep in same repo (manual releases only).\n  # During weekly releases, the intra-wave hook in release-plan.yml handles this.\n  self-update:\n    if: >-\n      github.event_name == 'release' &&\n      github.actor != 'github-actions[bot]' &&\n      contains(github.event.release.tag_name, 'websocket')\n    uses: gofiber/.github/.github/workflows/after-release.yml@main\n    with:\n      skip-wait: ${{ github.event_name == 'workflow_dispatch' }}\n      repos: |\n        - gofiber/contrib\n    secrets:\n      dispatch-token: ${{ secrets.DISPATCH_TOKEN }}\n\n  # Notify downstream repos. Runs for manual releases and weekly batch dispatch.\n  notify-dependents:\n    if: github.event_name != 'release' || github.actor != 'github-actions[bot]'\n    uses: gofiber/.github/.github/workflows/after-release.yml@main\n    with:\n      skip-wait: ${{ github.event_name == 'workflow_dispatch' }}\n      repos: |\n        - gofiber/recipes\n    secrets:\n      dispatch-token: ${{ secrets.DISPATCH_TOKEN }}\n"
  },
  {
    "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  workflow_dispatch:\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/.github\n"
  },
  {
    "path": ".github/workflows/cleanup-release-draft.yml",
    "content": "name: Cleanup Release Draft\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Tag or name of the draft (empty = latest draft)'\n        required: false\n        type: string\n        default: ''\n\njobs:\n  cleanup:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: gofiber/.github/.github/actions/cleanup-release-draft@main\n        with:\n          tag: ${{ inputs.tag }}\n"
  },
  {
    "path": ".github/workflows/dependabot-on-demand.yml",
    "content": "name: Dependabot On-Demand\n\non:\n  repository_dispatch:\n    types: [trigger-dependabot]\n  workflow_dispatch:\n\njobs:\n  trigger:\n    uses: gofiber/.github/.github/workflows/dependabot-on-demand.yml@main\n    secrets:\n      push-token: ${{ secrets.PR_TOKEN }}\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        id: wait_for_checks\n        uses: poseidon/wait-for-status-checks@v0.6.0\n        with:\n          token: ${{ secrets.PR_TOKEN }}\n          match_pattern: Tests\n          interval: 10\n          timeout: 600\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@v3.1.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/lint.yml",
    "content": "name: Linter\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths-ignore:\n      - \"**/*.md\"\n      - LICENSE\n      - \".github/ISSUE_TEMPLATE/*.yml\"\n      - \".github/dependabot.yml\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n      - LICENSE\n      - \".github/ISSUE_TEMPLATE/*.yml\"\n      - \".github/dependabot.yml\"\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pull-requests: read\n  checks: write\n\njobs:\n  lint:\n    uses: gofiber/.github/.github/workflows/go-lint.yml@main\n    with:\n      repo-type: multi\n      module-dir: v3\n      golangci-lint-args: \"--tests=false --timeout=5m\"\n"
  },
  {
    "path": ".github/workflows/release-drafter.yml",
    "content": "name: Release Drafter (v3 packages)\n\non:\n    push:\n        branches:\n            - master\n            - main\n    workflow_dispatch:\n\npermissions:\n    contents: read\n\njobs:\n    changes:\n        runs-on: ubuntu-latest\n        permissions:\n            contents: read\n            pull-requests: read\n        outputs:\n            packages: ${{ github.event_name == 'workflow_dispatch' && steps.filter-setup.outputs.packages || steps.map-packages.outputs.packages || '[]' }}\n        steps:\n            - name: Checkout repository\n              uses: actions/checkout@v6\n\n            - name: Generate filters\n              id: filter-setup\n              shell: bash\n              run: |\n                  shopt -s nullglob\n                  packages=(v3/*/)\n                  package_names=()\n\n                  if (( ${#packages[@]} == 0 )); then\n                      echo \"filters={}\" >> \"$GITHUB_OUTPUT\"\n                      echo \"packages=[]\" >> \"$GITHUB_OUTPUT\"\n                      exit 0\n                  fi\n\n                  {\n                      echo \"filters<<EOF\"\n                      for pkg in \"${packages[@]}\"; do\n                          name=${pkg#v3/}\n                          name=${name%/}\n                          path=${pkg%/}\n                          printf \"'%s': '%s/**'\\n\" \"$name\" \"$path\"\n                          package_names+=(\"$path\")\n                      done\n                      echo \"EOF\"\n                  } >> \"$GITHUB_OUTPUT\"\n\n                  if (( ${#package_names[@]} > 0 )); then\n                      printf -v joined '\"%s\",' \"${package_names[@]}\"\n                      joined=${joined%,}\n                      echo \"packages=[${joined}]\" >> \"$GITHUB_OUTPUT\"\n                  else\n                      echo \"packages=[]\" >> \"$GITHUB_OUTPUT\"\n                  fi\n\n            - name: Filter changes\n              id: filter\n              uses: dorny/paths-filter@v4\n              if: github.event_name != 'workflow_dispatch'\n              with:\n                  filters: ${{ steps.filter-setup.outputs.filters }}\n\n            - name: Map package paths\n              id: map-packages\n              if: github.event_name != 'workflow_dispatch'\n              env:\n                  FILTER_PACKAGES: ${{ steps.filter.outputs.changes || '[]' }}\n              run: |\n                  python3 - <<'PY' >> \"$GITHUB_OUTPUT\"\n                  import json\n                  import os\n\n                  packages = json.loads(os.environ[\"FILTER_PACKAGES\"])\n                  paths = [f\"v3/{name}\" for name in packages]\n                  print(f\"packages={json.dumps(paths)}\")\n                  PY\n\n    release-drafter:\n        needs: changes\n        runs-on: ubuntu-latest\n        timeout-minutes: 30\n        if: needs.changes.outputs.packages != '[]'\n        permissions:\n            contents: write\n            pull-requests: read\n        strategy:\n            matrix:\n                package: ${{ fromJSON(needs.changes.outputs.packages || '[]') }}\n        steps:\n            - name: Checkout shared config\n              uses: actions/checkout@v6\n              with:\n                  repository: gofiber/.github\n                  sparse-checkout: .github/release-drafter-module.yml\n                  sparse-checkout-cone-mode: false\n\n            - name: Generate config from shared template\n              run: |\n                  sed \"s|{{MODULE}}|${{ matrix.package }}|g\" \\\n                      .github/release-drafter-module.yml > .github/release-drafter-parsed.yml\n\n            - name: Run release drafter\n              uses: release-drafter/release-drafter@v7\n              with:\n                  config-name: file:release-drafter-parsed.yml\n"
  },
  {
    "path": ".github/workflows/sync-docs.yml",
    "content": "name: Sync docs\n\non:\n  push:\n    branches: [main, master]\n    paths: ['**/*.md']\n  release:\n    types: [published]\n  repository_dispatch:\n    types: [sync-docs]\n  workflow_dispatch:\n    inputs:\n      mode:\n        description: 'push = sync current docs, release-all = version snapshots for all latest module releases'\n        type: choice\n        options: [push, release-all]\n        default: push\n\njobs:\n  sync:\n    uses: gofiber/.github/.github/workflows/sync-docs.yml@main\n    with:\n      repo-type: multi\n      source-dir: v3\n      destination-dir: docs/contrib\n      version-file: contrib_versions.json\n      docusaurus-command: npm run docusaurus -- docs:version:contrib\n      commit-url: https://github.com/gofiber/contrib\n      event-mode: >-\n        ${{ github.event_name == 'workflow_dispatch' && inputs.mode\n        || github.event_name == 'repository_dispatch' && 'release-all'\n        || github.event_name }}\n      tag-name: ${{ github.ref_name }}\n    secrets:\n      doc-sync-token: ${{ secrets.DOC_SYNC_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test-casbin.yml",
    "content": "name: \"Test casbin\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/casbin/**/*.go'\n      - 'v3/casbin/go.mod'\n  pull_request:\n    paths:\n      - 'v3/casbin/**/*.go'\n      - 'v3/casbin/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/casbin\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-circuitbreaker.yml",
    "content": "name: \"Test CircuitBreaker\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/circuitbreaker/**/*.go'\n      - 'v3/circuitbreaker/go.mod'\n  pull_request:\n    paths:\n      - 'v3/circuitbreaker/**/*.go'\n      - 'v3/circuitbreaker/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/circuitbreaker\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-coraza.yml",
    "content": "name: \"Test Coraza\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/coraza/**/*.go'\n      - 'v3/coraza/go.mod'\n  pull_request:\n    paths:\n      - 'v3/coraza/**/*.go'\n      - 'v3/coraza/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.26.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/coraza\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-fgprof.yml",
    "content": "name: \"Test Fgprof\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/fgprof/**/*.go'\n      - 'v3/fgprof/go.mod'\n  pull_request:\n    paths:\n      - 'v3/fgprof/**/*.go'\n      - 'v3/fgprof/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/fgprof\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-hcaptcha.yml",
    "content": "name: \"Test hcaptcha\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/hcaptcha/**/*.go'\n      - 'v3/hcaptcha/go.mod'\n  pull_request:\n    paths:\n      - 'v3/hcaptcha/**/*.go'\n      - 'v3/hcaptcha/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/hcaptcha\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-i18n.yml",
    "content": "name: \"Test i18n\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/i18n/**/*.go'\n      - 'v3/i18n/go.mod'\n  pull_request:\n    paths:\n      - 'v3/i18n/**/*.go'\n      - 'v3/i18n/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n          - 1.26.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/i18n\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-jwt.yml",
    "content": "name: \"Test jwt\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/jwt/**/*.go'\n      - 'v3/jwt/go.mod'\n  pull_request:\n    paths:\n      - 'v3/jwt/**/*.go'\n      - 'v3/jwt/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/jwt\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-loadshed.yml",
    "content": "name: \"Test Loadshed\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/loadshed/**/*.go'\n      - 'v3/loadshed/go.mod'\n  pull_request:\n    paths:\n      - 'v3/loadshed/**/*.go'\n      - 'v3/loadshed/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n          - 1.26.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: \"${{ matrix.go-version }}\"\n      - name: Run Test\n        working-directory: ./v3/loadshed\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-monitor.yml",
    "content": "name: \"Test Monitor\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/monitor/**/*.go'\n      - 'v3/monitor/go.mod'\n  pull_request:\n    paths:\n      - 'v3/monitor/**/*.go'\n      - 'v3/monitor/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/monitor\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-newrelic.yml",
    "content": "name: \"Test newrelic\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/newrelic/**/*.go'\n      - 'v3/newrelic/go.mod'\n  pull_request:\n    paths:\n      - 'v3/newrelic/**/*.go'\n      - 'v3/newrelic/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/newrelic\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-opa.yml",
    "content": "name: \"Test opa\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/opa/**/*.go'\n      - 'v3/opa/go.mod'\n  pull_request:\n    paths:\n      - 'v3/opa/**/*.go'\n      - 'v3/opa/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/opa\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-otel.yml",
    "content": "name: \"Test otel\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/otel/**/*.go'\n      - 'v3/otel/go.mod'\n  pull_request:\n    paths:\n      - 'v3/otel/**/*.go'\n      - 'v3/otel/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n          - 1.26.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/otel\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-paseto.yml",
    "content": "name: \"Test paseto\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/paseto/**/*.go'\n      - 'v3/paseto/go.mod'\n  pull_request:\n    paths:\n      - 'v3/paseto/**/*.go'\n      - 'v3/paseto/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/paseto\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-sentry.yml",
    "content": "name: \"Test sentry\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/sentry/**/*.go'\n      - 'v3/sentry/go.mod'\n  pull_request:\n    paths:\n      - 'v3/sentry/**/*.go'\n      - 'v3/sentry/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/sentry\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-socketio.yml",
    "content": "name: \"Test Socket.io\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/socketio/**/*.go'\n      - 'v3/socketio/go.mod'\n  pull_request:\n    paths:\n      - 'v3/socketio/**/*.go'\n      - 'v3/socketio/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: \"${{ matrix.go-version }}\"\n      - name: Run Test\n        working-directory: ./v3/socketio\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-swaggerui.yml",
    "content": "name: \"Test swaggerui\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/swaggerui/**/*.go'\n      - 'v3/swaggerui/go.mod'\n      - 'v3/swaggerui/go.sum'\n  pull_request:\n    paths:\n      - 'v3/swaggerui/**/*.go'\n      - 'v3/swaggerui/go.mod'\n      - 'v3/swaggerui/go.sum'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/swaggerui\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-swaggo.yml",
    "content": "name: \"Test swaggo\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/swaggo/**/*.go'\n      - 'v3/swaggo/go.mod'\n      - 'v3/swaggo/go.sum'\n  pull_request:\n    paths:\n      - 'v3/swaggo/**/*.go'\n      - 'v3/swaggo/go.mod'\n      - 'v3/swaggo/go.sum'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/swaggo\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-testcontainers.yml",
    "content": "name: \"Test Testcontainers Services\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/testcontainers/**/*.go'\n      - 'v3/testcontainers/go.mod'\n  pull_request:\n    paths:\n      - 'v3/testcontainers/**/*.go'\n      - 'v3/testcontainers/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n\n      - name: Run Test\n        working-directory: ./v3/testcontainers\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-websocket.yml",
    "content": "name: \"Test websocket\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/websocket/**/*.go'\n      - 'v3/websocket/go.mod'\n  pull_request:\n    paths:\n      - 'v3/websocket/**/*.go'\n      - 'v3/websocket/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n          - 1.26.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/websocket\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-zap.yml",
    "content": "name: \"Test zap\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/zap/**/*.go'\n      - 'v3/zap/go.mod'\n  pull_request:\n    paths:\n      - 'v3/zap/**/*.go'\n      - 'v3/zap/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/zap\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/test-zerolog.yml",
    "content": "name: \"Test zerolog\"\n\non:\n  push:\n    branches:\n      - master\n      - main\n    paths:\n      - 'v3/zerolog/**/*.go'\n      - 'v3/zerolog/go.mod'\n  pull_request:\n    paths:\n      - 'v3/zerolog/**/*.go'\n      - 'v3/zerolog/go.mod'\n\n  workflow_dispatch:\njobs:\n  Tests:\n    runs-on: ubuntu-latest\n    env:\n      GOWORK: off\n    strategy:\n      matrix:\n        go-version:\n          - 1.25.x\n    steps:\n      - name: Fetch Repository\n        uses: actions/checkout@v6\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '${{ matrix.go-version }}'\n      - name: Run Test\n        working-directory: ./v3/zerolog\n        run: go test -v -race ./...\n"
  },
  {
    "path": ".github/workflows/weekly-release.yml",
    "content": "name: Weekly release\n\non:\n  schedule:\n    - cron: '0 8 * * 4'\n  workflow_dispatch:\n    inputs:\n      draft-tags:\n        description: >\n          Tags or package names to publish, comma-separated (e.g.\n          \"websocket,jwt\" or \"v3/websocket/v1.2.0\"). Matched as\n          substrings. Empty = all.\n        type: string\n        default: ''\n      dry-run:\n        description: Show diff but do not publish.\n        type: boolean\n        default: false\n      delay:\n        description: Minutes between module publishes (default 2).\n        type: string\n        default: '2'\n\nconcurrency:\n  group: weekly-release-${{ github.repository }}\n  cancel-in-progress: false\n\npermissions:\n  contents: write\n  pull-requests: read\n  issues: write\n\njobs:\n  release:\n    uses: gofiber/.github/.github/workflows/weekly-release.yml@main\n    with:\n      repo-type: multi\n      dry-run: ${{ inputs.dry-run || false }}\n      draft-tags: ${{ inputs.draft-tags || '' }}\n      publish-delay-minutes: ${{ inputs.delay || '2' }}\n    secrets:\n      dispatch-token: ${{ secrets.DISPATCH_TOKEN }}\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, specifically when used with LiteIDE\n*.out\n\n# IDE files\n.vscode\n.DS_Store\n.idea\n\n# Misc\n*.fiber.gz\n*.fasthttp.gz\n*.pprof\n*.workspace\n\n# Dependencies\nvendor\n/Godeps/\n\n# Go workspace file\ngo.work.sum\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Fiber\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "---\ntitle: 👋 Welcome\nsidebar_position: 1\n---\n\n> 📦 The canonical copy of this README lives in [v3/README.md](./v3/README.md).\n\n<div align=\"center\">\n  <img height=\"125\" alt=\"Fiber\" src=\"https://raw.githubusercontent.com/gofiber/contrib/master/.github/logo-dark.svg#gh-dark-mode-only\" />\n  <img height=\"125\" alt=\"Fiber\" src=\"https://raw.githubusercontent.com/gofiber/contrib/master/.github/logo.svg#gh-light-mode-only\" />\n  <br />\n\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Linter](https://github.com/gofiber/contrib/actions/workflows/lint.yml/badge.svg)\n\nRepository for third party middlewares and service implementations, with dependencies.\n\n> **Go version support:** We only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n</div>\n\n## 📑 Middleware Implementations\n\n* [casbin](./v3/casbin/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Casbin%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-casbin.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"casbin workflow status\" /> </a>\n* [circuitbreaker](./v3/circuitbreaker/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+CircuitBreaker%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-circuitbreaker.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"circuitbreaker workflow status\" /> </a>\n* [fgprof](./v3/fgprof/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Fgprof%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-fgprof.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"fgprof workflow status\" /> </a>\n* [i18n](./v3/i18n/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+i18n%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-i18n.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"i18n workflow status\" /> </a>\n* [sentry](./v3/sentry/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+sentry%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-sentry.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"sentry workflow status\" /> </a>\n* [zap](./v3/zap/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+zap%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-zap.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"zap workflow status\" /> </a>\n* [zerolog](./v3/zerolog/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+zerolog%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-zerolog.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"zerolog workflow status\" /> </a>\n* [hcaptcha](./v3/hcaptcha/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+hcaptcha%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-hcaptcha.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"hcaptcha workflow status\" /> </a>\n* [jwt](./v3/jwt/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+jwt%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-jwt.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"jwt workflow status\" /> </a>\n* [loadshed](./v3/loadshed/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+loadshed%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-loadshed.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"loadshed workflow status\" /> </a>\n* [new relic](./v3/newrelic/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+newrelic%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-newrelic.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"new relic workflow status\" /> </a>\n* [monitor](./v3/monitor/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Monitor%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-monitor.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"monitor workflow status\" /> </a>\n* [open policy agent](./v3/opa/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+opa%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-opa.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"OPA workflow status\" /> </a>\n* [otel (opentelemetry)](./v3/otel/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+otel%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-otel.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"otel workflow status\" /> </a>\n* [paseto](./v3/paseto/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+paseto%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-paseto.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"paseto workflow status\" /> </a>\n* [socket.io](./v3/socketio/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+socketio%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-socketio.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"socket.io workflow status\" /> </a>\n* [swaggo](./v3/swaggo/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+swaggo%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-swaggo.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"swaggo workflow status\" /> </a> _(formerly `gofiber/swagger`)_\n* [swaggerui](./v3/swaggerui/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+swaggerui%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-swaggerui.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"swaggerui workflow status\" /> </a> _(formerly `gofiber/contrib/swagger`)_\n* [websocket](./v3/websocket/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+websocket%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-websocket.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"websocket workflow status\" /> </a>\n\n## 🥡 Service Implementations\n\n* [testcontainers](./v3/testcontainers/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Testcontainers%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-testcontainers.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"testcontainers workflow status\" /> </a>\n"
  },
  {
    "path": "go.work",
    "content": "go 1.26.1\n\nuse (\n\t./v3/casbin\n\t./v3/circuitbreaker\n\t./v3/coraza\n\t./v3/fgprof\n\t./v3/hcaptcha\n\t./v3/i18n\n\t./v3/jwt\n\t./v3/loadshed\n\t./v3/monitor\n\t./v3/newrelic\n\t./v3/opa\n\t./v3/otel\n\t./v3/paseto\n\t./v3/sentry\n\t./v3/socketio\n\t./v3/swaggerui\n\t./v3/swaggo\n\t./v3/testcontainers\n\t./v3/websocket\n\t./v3/zap\n\t./v3/zerolog\n)\n"
  },
  {
    "path": "v3/.golangci.yml",
    "content": "version: \"2\"\n\nrun:\n  go: \"1.25\"\n  timeout: 5m\n  tests: false\n"
  },
  {
    "path": "v3/README.md",
    "content": "---\ntitle: 👋 Welcome\nsidebar_position: 1\n---\n\n<div align=\"center\">\n  <img height=\"125\" alt=\"Fiber\" src=\"https://raw.githubusercontent.com/gofiber/contrib/master/.github/logo-dark.svg#gh-dark-mode-only\" />\n  <img height=\"125\" alt=\"Fiber\" src=\"https://raw.githubusercontent.com/gofiber/contrib/master/.github/logo.svg#gh-light-mode-only\" />\n  <br />\n\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Linter](https://github.com/gofiber/contrib/actions/workflows/lint.yml/badge.svg)\n\nRepository for third party middlewares and service implementations, with dependencies.\n\n> **Go version support:** We only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n</div>\n\n## 📑 Middleware Implementations\n\n* [casbin](./casbin/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Casbin%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-casbin.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"casbin workflow status\" /> </a>\n* [circuitbreaker](./circuitbreaker/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+CircuitBreaker%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-circuitbreaker.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"circuitbreaker workflow status\" /> </a>\n* [coraza](./coraza/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Coraza%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-coraza.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"coraza workflow status\" /> </a>\n* [fgprof](./fgprof/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Fgprof%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-fgprof.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"fgprof workflow status\" /> </a>\n* [i18n](./i18n/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+i18n%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-i18n.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"i18n workflow status\" /> </a>\n* [sentry](./sentry/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+sentry%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-sentry.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"sentry workflow status\" /> </a>\n* [zap](./zap/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+zap%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-zap.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"zap workflow status\" /> </a>\n* [zerolog](./zerolog/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+zerolog%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-zerolog.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"zerolog workflow status\" /> </a>\n* [hcaptcha](./hcaptcha/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+hcaptcha%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-hcaptcha.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"hcaptcha workflow status\" /> </a>\n* [jwt](./jwt/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+jwt%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-jwt.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"jwt workflow status\" /> </a>\n* [loadshed](./loadshed/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+loadshed%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-loadshed.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"loadshed workflow status\" /> </a>\n* [new relic](./newrelic/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+newrelic%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-newrelic.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"new relic workflow status\" /> </a>\n* [monitor](./monitor/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Monitor%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-monitor.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"monitor workflow status\" /> </a>\n* [open policy agent](./opa/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+opa%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-opa.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"OPA workflow status\" /> </a>\n* [otel (opentelemetry)](./otel/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+otel%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-otel.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"otel workflow status\" /> </a>\n* [paseto](./paseto/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+paseto%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-paseto.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"paseto workflow status\" /> </a>\n* [socket.io](./socketio/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+socketio%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-socketio.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"socket.io workflow status\" /> </a>\n* [swaggo](./swaggo/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+swaggo%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-swaggo.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"swaggo workflow status\" /> </a> _(formerly `gofiber/swagger`)_\n* [swaggerui](./swaggerui/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+swaggerui%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-swaggerui.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"swaggerui workflow status\" /> </a> _(formerly `gofiber/contrib/swagger`)_\n* [websocket](./websocket/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+websocket%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-websocket.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"websocket workflow status\" /> </a>\n\n## 🥡 Service Implementations\n\n* [testcontainers](./testcontainers/README.md) <a href=\"https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Testcontainers%22\"> <img src=\"https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-testcontainers.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B\" alt=\"testcontainers workflow status\" /> </a>\n"
  },
  {
    "path": "v3/casbin/README.md",
    "content": "---\nid: casbin\n---\n\n# Casbin\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*casbin*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20casbin/badge.svg)\n\nCasbin middleware for Fiber.\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/casbin\n```\nchoose an adapter from [here](https://casbin.org/docs/adapters)\n```sh\ngo get -u github.com/casbin/xorm-adapter\n```\n\n## Signature\n```go\ncasbin.New(config ...casbin.Config) *casbin.Middleware\n```\n\n## Config\n\n| Property      | Type                      | Description                              | Default                              |\n|:--------------|:--------------------------|:-----------------------------------------|:--------------------------------------------------------------|\n| ModelFilePath | `string`                  | Model file path                        | `\"./model.conf\"`                                                    |\n| PolicyAdapter | `persist.Adapter`         | Database adapter for policies            | `./policy.csv`                                                      |\n| Enforcer      | `*casbin.Enforcer`        | Custom casbin enforcer                   | `Middleware generated enforcer using ModelFilePath & PolicyAdapter` |\n| Lookup        | `func(fiber.Ctx) string`  | Look up for current subject              | `\"\"`                                                              |\n| Unauthorized  | `func(fiber.Ctx) error`   | Response body for unauthorized responses | `Unauthorized`                                                      |\n| Forbidden     | `func(fiber.Ctx) error`   | Response body for forbidden responses    | `Forbidden`                                                         |\n\n### Examples\n- [Gorm Adapter](https://github.com/svcg/-fiber_casbin_demo)\n- [File Adapter](https://github.com/gofiber/contrib/tree/master/v3/casbin/example)\n\n## CustomPermission\n\n```go\npackage main\n\nimport (\n  \"github.com/gofiber/fiber/v3\"\n  \"github.com/gofiber/contrib/v3/casbin\"\n  _ \"github.com/go-sql-driver/mysql\"\n  \"github.com/casbin/xorm-adapter/v2\"\n)\n\nfunc main() {\n  app := fiber.New()\n\n  authz := casbin.New(casbin.Config{\n      ModelFilePath: \"path/to/rbac_model.conf\",\n      PolicyAdapter: xormadapter.NewAdapter(\"mysql\", \"root:@tcp(127.0.0.1:3306)/\"),\n      Lookup: func(c fiber.Ctx) string {\n          // fetch authenticated user subject\n      },\n  })\n\n  app.Post(\"/blog\",\n      authz.RequiresPermissions([]string{\"blog:create\"}, casbin.WithValidationRule(casbin.MatchAllRule)),\n      func(c fiber.Ctx) error {\n        // your handler\n      },\n  )\n  \n  app.Delete(\"/blog/:id\",\n    authz.RequiresPermissions([]string{\"blog:create\", \"blog:delete\"}, casbin.WithValidationRule(casbin.AtLeastOneRule)),\n    func(c fiber.Ctx) error {\n      // your handler\n    },\n  )\n\n  app.Listen(\":8080\")\n}\n```\n\n## RoutePermission\n\n```go\npackage main\n\nimport (\n  \"github.com/gofiber/fiber/v3\"\n  \"github.com/gofiber/contrib/v3/casbin\"\n  _ \"github.com/go-sql-driver/mysql\"\n  \"github.com/casbin/xorm-adapter/v2\"\n)\n\nfunc main() {\n  app := fiber.New()\n\n  authz := casbin.New(casbin.Config{\n      ModelFilePath: \"path/to/rbac_model.conf\",\n      PolicyAdapter: xormadapter.NewAdapter(\"mysql\", \"root:@tcp(127.0.0.1:3306)/\"),\n      Lookup: func(c fiber.Ctx) string {\n          // fetch authenticated user subject\n      },\n  })\n\n  // check permission with Method and Path\n  app.Post(\"/blog\",\n    authz.RoutePermission(),\n    func(c fiber.Ctx) error {\n      // your handler\n    },\n  )\n\n  app.Listen(\":8080\")\n}\n```\n\n## RoleAuthorization\n\n```go\npackage main\n\nimport (\n  \"github.com/gofiber/fiber/v3\"\n  \"github.com/gofiber/contrib/v3/casbin\"\n  _ \"github.com/go-sql-driver/mysql\"\n  \"github.com/casbin/xorm-adapter/v2\"\n)\n\nfunc main() {\n  app := fiber.New()\n\n  authz := casbin.New(casbin.Config{\n      ModelFilePath: \"path/to/rbac_model.conf\",\n      PolicyAdapter: xormadapter.NewAdapter(\"mysql\", \"root:@tcp(127.0.0.1:3306)/\"),\n      Lookup: func(c fiber.Ctx) string {\n          // fetch authenticated user subject\n      },\n  })\n  \n  app.Put(\"/blog/:id\",\n    authz.RequiresRoles([]string{\"admin\"}),\n    func(c fiber.Ctx) error {\n      // your handler\n    },\n  )\n\n  app.Listen(\":8080\")\n}\n```\n"
  },
  {
    "path": "v3/casbin/casbin.go",
    "content": "package casbin\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Middleware ...\ntype Middleware struct {\n\tconfig Config\n}\n\n// New creates an authorization middleware for use in Fiber\nfunc New(config ...Config) *Middleware {\n\tcfg, err := configDefault(config...)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"fiber: casbin middleware error -> %w\", err))\n\t}\n\n\treturn &Middleware{\n\t\tconfig: cfg,\n\t}\n}\n\n// RequiresPermissions tries to find the current subject and determine if the\n// subject has the required permissions according to predefined Casbin policies.\nfunc (m *Middleware) RequiresPermissions(permissions []string, opts ...Option) fiber.Handler {\n\toptions := optionsDefault(opts...)\n\n\treturn func(c fiber.Ctx) error {\n\t\tif len(permissions) == 0 {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tsub := m.config.Lookup(c)\n\t\tif len(sub) == 0 {\n\t\t\treturn m.config.Unauthorized(c)\n\t\t}\n\n\t\tswitch options.ValidationRule {\n\t\tcase MatchAllRule:\n\t\t\tfor _, permission := range permissions {\n\t\t\t\tvals := append([]string{sub}, options.PermissionParser(permission)...)\n\t\t\t\tif ok, err := m.config.Enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil {\n\t\t\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t\t\t} else if !ok {\n\t\t\t\t\treturn m.config.Forbidden(c)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn c.Next()\n\t\tcase AtLeastOneRule:\n\t\t\tfor _, permission := range permissions {\n\t\t\t\tvals := append([]string{sub}, options.PermissionParser(permission)...)\n\t\t\t\tif ok, err := m.config.Enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil {\n\t\t\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t\t\t} else if ok {\n\t\t\t\t\treturn c.Next()\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn m.config.Forbidden(c)\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n\n// RoutePermission tries to find the current subject and determine if the\n// subject has the required permissions according to predefined Casbin policies.\n// This method uses http Path and Method as object and action.\nfunc (m *Middleware) RoutePermission() fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\tsub := m.config.Lookup(c)\n\t\tif len(sub) == 0 {\n\t\t\treturn m.config.Unauthorized(c)\n\t\t}\n\n\t\tif ok, err := m.config.Enforcer.Enforce(sub, c.Path(), c.Method()); err != nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t} else if !ok {\n\t\t\treturn m.config.Forbidden(c)\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n\n// RequiresRoles tries to find the current subject and determine if the\n// subject has the required roles according to predefined Casbin policies.\nfunc (m *Middleware) RequiresRoles(roles []string, opts ...Option) fiber.Handler {\n\toptions := optionsDefault(opts...)\n\n\treturn func(c fiber.Ctx) error {\n\t\tif len(roles) == 0 {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tsub := m.config.Lookup(c)\n\t\tif len(sub) == 0 {\n\t\t\treturn m.config.Unauthorized(c)\n\t\t}\n\n\t\tuserRoles, err := m.config.Enforcer.GetRolesForUser(sub)\n\t\tif err != nil {\n\t\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t\t}\n\n\t\tswitch options.ValidationRule {\n\t\tcase MatchAllRule:\n\t\t\tfor _, role := range roles {\n\t\t\t\tif !containsString(userRoles, role) {\n\t\t\t\t\treturn m.config.Forbidden(c)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn c.Next()\n\t\tcase AtLeastOneRule:\n\t\t\tfor _, role := range roles {\n\t\t\t\tif containsString(userRoles, role) {\n\t\t\t\t\treturn c.Next()\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn m.config.Forbidden(c)\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n"
  },
  {
    "path": "v3/casbin/casbin_test.go",
    "content": "package casbin\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/casbin/casbin/v2\"\n\n\t\"github.com/casbin/casbin/v2/model\"\n\t\"github.com/casbin/casbin/v2/persist\"\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nvar (\n\tsubjectAlice = func(c fiber.Ctx) string { return \"alice\" }\n\tsubjectBob   = func(c fiber.Ctx) string { return \"bob\" }\n\tsubjectEmpty = func(c fiber.Ctx) string { return \"\" }\n)\n\nconst (\n\tmodelConf = `\n\t[request_definition]\n\tr = sub, obj, act\n\t\n\t[policy_definition]\n\tp = sub, obj, act\n\t\n\t[role_definition]\n\tg = _, _\n\t\n\t[policy_effect]\n\te = some(where (p.eft == allow))\n\t\n\t[matchers]\n\tm = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act`\n\n\tpolicyList = `\n\tp,admin,blog,create\n\tp,admin,blog,update\n\tp,admin,blog,delete\n\tp,user,comment,create\n\tp,user,comment,delete\n\n\tp,admin,/blog,POST\n\tp,admin,/blog/1,PUT\n\tp,admin,/blog/1,DELETE\n\tp,user,/comment,POST\n\n\n\tg,alice,admin\n\tg,alice,user\n\tg,bob,user`\n)\n\n// mockAdapter\ntype mockAdapter struct {\n\ttext string\n}\n\nfunc newMockAdapter(text string) *mockAdapter {\n\treturn &mockAdapter{\n\t\ttext: text,\n\t}\n}\n\nfunc (ma *mockAdapter) LoadPolicy(model model.Model) error {\n\tif ma.text == \"\" {\n\t\treturn errors.New(\"text is required\")\n\t}\n\n\tstrs := strings.Split(ma.text, \"\\n\")\n\n\tfor _, str := range strs {\n\t\tif str == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tpersist.LoadPolicyLine(str, model)\n\t}\n\n\treturn nil\n}\n\nfunc (ma *mockAdapter) SavePolicy(model model.Model) error {\n\treturn errors.New(\"not implemented\")\n}\n\nfunc (ma *mockAdapter) AddPolicy(sec string, ptype string, rule []string) error {\n\treturn errors.New(\"not implemented\")\n}\n\nfunc (ma *mockAdapter) RemovePolicy(sec string, ptype string, rule []string) error {\n\treturn errors.New(\"not implemented\")\n}\n\nfunc (ma *mockAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {\n\treturn errors.New(\"not implemented\")\n}\n\nfunc setup() (*casbin.Enforcer, error) {\n\tm, err := model.NewModelFromString(modelConf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tenf, err := casbin.NewEnforcer(m, newMockAdapter(policyList))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn enf, nil\n}\n\nfunc Test_RequiresPermission(t *testing.T) {\n\tenf, err := setup()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tdesc        string\n\t\tlookup      func(fiber.Ctx) string\n\t\tpermissions []string\n\t\topts        []Option\n\t\tstatusCode  int\n\t}{\n\t\t{\n\t\t\tdesc:        \"alice has permission to create blog\",\n\t\t\tlookup:      subjectAlice,\n\t\t\tpermissions: []string{\"blog:create\"},\n\t\t\topts:        []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode:  200,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"alice has permission to create blog\",\n\t\t\tlookup:      subjectAlice,\n\t\t\tpermissions: []string{\"blog:create\"},\n\t\t\topts:        []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode:  200,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"alice has permission to create and update blog\",\n\t\t\tlookup:      subjectAlice,\n\t\t\tpermissions: []string{\"blog:create\", \"blog:update\"},\n\t\t\topts:        []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode:  200,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"alice has permission to create comment or blog\",\n\t\t\tlookup:      subjectAlice,\n\t\t\tpermissions: []string{\"comment:create\", \"blog:create\"},\n\t\t\topts:        []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode:  200,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"bob has only permission to create comment\",\n\t\t\tlookup:      subjectBob,\n\t\t\tpermissions: []string{\"comment:create\", \"blog:create\"},\n\t\t\topts:        []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode:  200,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"unauthenticated user has no permissions\",\n\t\t\tlookup:      subjectEmpty,\n\t\t\tpermissions: []string{\"comment:create\"},\n\t\t\topts:        []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode:  401,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"bob has not permission to create blog\",\n\t\t\tlookup:      subjectBob,\n\t\t\tpermissions: []string{\"blog:create\"},\n\t\t\topts:        []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode:  403,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"bob has not permission to delete blog\",\n\t\t\tlookup:      subjectBob,\n\t\t\tpermissions: []string{\"blog:delete\"},\n\t\t\topts:        []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode:  403,\n\t\t},\n\t\t{\n\t\t\tdesc:        \"invalid permission\",\n\t\t\tlookup:      subjectBob,\n\t\t\tpermissions: []string{\"unknown\"},\n\t\t\topts:        []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode:  500,\n\t\t},\n\t}\n\n\tfor _, tC := range testCases {\n\t\tapp := fiber.New()\n\n\t\tauthz := New(Config{\n\t\t\tEnforcer: enf,\n\t\t\tLookup:   tC.lookup,\n\t\t})\n\n\t\tapp.Post(\"/blog\",\n\t\t\tauthz.RequiresPermissions(tC.permissions, tC.opts...),\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t},\n\t\t)\n\n\t\tt.Run(tC.desc, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(\"POST\", \"/blog\", nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treq.Host = \"localhost\"\n\n\t\t\tresp, err := app.Test(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif resp.StatusCode != tC.statusCode {\n\t\t\t\tt.Errorf(`StatusCode: got %v - expected %v`, resp.StatusCode, tC.statusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_RequiresRoles(t *testing.T) {\n\tenf, err := setup()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\tlookup     func(fiber.Ctx) string\n\t\troles      []string\n\t\topts       []Option\n\t\tstatusCode int\n\t}{\n\t\t{\n\t\t\tdesc:       \"alice has user role\",\n\t\t\tlookup:     subjectAlice,\n\t\t\troles:      []string{\"user\"},\n\t\t\topts:       []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"alice has admin role\",\n\t\t\tlookup:     subjectAlice,\n\t\t\troles:      []string{\"admin\"},\n\t\t\topts:       []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"alice has both user and admin roles\",\n\t\t\tlookup:     subjectAlice,\n\t\t\troles:      []string{\"user\", \"admin\"},\n\t\t\topts:       []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"alice has both user and admin roles\",\n\t\t\tlookup:     subjectAlice,\n\t\t\troles:      []string{\"user\", \"admin\"},\n\t\t\topts:       []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"bob has only user role\",\n\t\t\tlookup:     subjectBob,\n\t\t\troles:      []string{\"user\"},\n\t\t\topts:       []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"unauthenticated user has no permissions\",\n\t\t\tlookup:     subjectEmpty,\n\t\t\troles:      []string{\"user\"},\n\t\t\topts:       []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode: 401,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"bob has not admin role\",\n\t\t\tlookup:     subjectBob,\n\t\t\troles:      []string{\"admin\"},\n\t\t\topts:       []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode: 403,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"bob has only user role\",\n\t\t\tlookup:     subjectBob,\n\t\t\troles:      []string{\"admin\", \"user\"},\n\t\t\topts:       []Option{WithValidationRule(AtLeastOneRule)},\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"invalid role\",\n\t\t\tlookup:     subjectBob,\n\t\t\troles:      []string{\"unknown\"},\n\t\t\topts:       []Option{WithValidationRule(MatchAllRule)},\n\t\t\tstatusCode: 403,\n\t\t},\n\t}\n\n\tfor _, tC := range testCases {\n\t\tapp := fiber.New()\n\n\t\tauthz := New(Config{\n\t\t\tEnforcer: enf,\n\t\t\tLookup:   tC.lookup,\n\t\t})\n\n\t\tapp.Post(\"/blog\",\n\t\t\tauthz.RequiresRoles(tC.roles, tC.opts...),\n\t\t\tfunc(c fiber.Ctx) error {\n\t\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t\t},\n\t\t)\n\n\t\tt.Run(tC.desc, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(\"POST\", \"/blog\", nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treq.Host = \"localhost\"\n\n\t\t\tresp, err := app.Test(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif resp.StatusCode != tC.statusCode {\n\t\t\t\tt.Errorf(`StatusCode: got %v - expected %v`, resp.StatusCode, tC.statusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_RoutePermission(t *testing.T) {\n\tenf, err := setup()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\tdesc       string\n\t\turl        string\n\t\tmethod     string\n\t\tsubject    string\n\t\tstatusCode int\n\t}{\n\t\t{\n\t\t\tdesc:       \"alice has permission to create blog\",\n\t\t\turl:        \"/blog\",\n\t\t\tmethod:     \"POST\",\n\t\t\tsubject:    \"alice\",\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"alice has permission to update blog\",\n\t\t\turl:        \"/blog/1\",\n\t\t\tmethod:     \"PUT\",\n\t\t\tsubject:    \"alice\",\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"bob has only permission to create comment\",\n\t\t\turl:        \"/comment\",\n\t\t\tmethod:     \"POST\",\n\t\t\tsubject:    \"bob\",\n\t\t\tstatusCode: 200,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"unauthenticated user has no permissions\",\n\t\t\turl:        \"/\",\n\t\t\tmethod:     \"POST\",\n\t\t\tsubject:    \"\",\n\t\t\tstatusCode: 401,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"bob has not permission to create blog\",\n\t\t\turl:        \"/blog\",\n\t\t\tmethod:     \"POST\",\n\t\t\tsubject:    \"bob\",\n\t\t\tstatusCode: 403,\n\t\t},\n\t\t{\n\t\t\tdesc:       \"bob has not permission to delete blog\",\n\t\t\turl:        \"/blog/1\",\n\t\t\tmethod:     \"DELETE\",\n\t\t\tsubject:    \"bob\",\n\t\t\tstatusCode: 403,\n\t\t},\n\t}\n\n\tapp := fiber.New()\n\n\tauthz := New(Config{\n\t\tEnforcer: enf,\n\t\tLookup: func(c fiber.Ctx) string {\n\t\t\treturn c.Get(\"x-subject\")\n\t\t},\n\t})\n\n\tapp.Use(authz.RoutePermission())\n\n\tapp.Post(\"/blog\",\n\t\tfunc(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t},\n\t)\n\tapp.Put(\"/blog/:id\",\n\t\tfunc(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t},\n\t)\n\tapp.Delete(\"/blog/:id\",\n\t\tfunc(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t},\n\t)\n\tapp.Post(\"/comment\",\n\t\tfunc(c fiber.Ctx) error {\n\t\t\treturn c.SendStatus(fiber.StatusOK)\n\t\t},\n\t)\n\n\tfor _, tC := range testCases {\n\t\tt.Run(tC.desc, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(tC.method, tC.url, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treq.Host = \"localhost\"\n\n\t\t\treq.Header.Set(\"x-subject\", tC.subject)\n\t\t\tresp, err := app.Test(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif resp.StatusCode != tC.statusCode {\n\t\t\t\tt.Errorf(`StatusCode: got %v - expected %v`, resp.StatusCode, tC.statusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v3/casbin/config.go",
    "content": "package casbin\n\nimport (\n\t\"github.com/casbin/casbin/v2\"\n\t\"github.com/casbin/casbin/v2/persist\"\n\tfileadapter \"github.com/casbin/casbin/v2/persist/file-adapter\"\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// Config holds the configuration for the middleware\ntype Config struct {\n\t// ModelFilePath is path to model file for Casbin.\n\t// Optional. Default: \"./model.conf\".\n\tModelFilePath string\n\n\t// PolicyAdapter is an interface for different persistent providers.\n\t// Optional. Default: fileadapter.NewAdapter(\"./policy.csv\").\n\tPolicyAdapter persist.Adapter\n\n\t// Enforcer is an enforcer. If you want to use your own enforcer.\n\t// Optional. Default: nil\n\tEnforcer *casbin.Enforcer\n\n\t// Lookup is a function that is used to look up current subject.\n\t// An empty string is considered as unauthenticated user.\n\t// Optional. Default: func(c fiber.Ctx) string { return \"\" }\n\tLookup func(fiber.Ctx) string\n\n\t// Unauthorized defines the response body for unauthorized responses.\n\t// Optional. Default: func(c fiber.Ctx) error { return c.SendStatus(401) }\n\tUnauthorized fiber.Handler\n\n\t// Forbidden defines the response body for forbidden responses.\n\t// Optional. Default: func(c fiber.Ctx) error { return c.SendStatus(403) }\n\tForbidden fiber.Handler\n}\n\nvar ConfigDefault = Config{\n\tModelFilePath: \"./model.conf\",\n\tPolicyAdapter: fileadapter.NewAdapter(\"./policy.csv\"),\n\tLookup:        func(c fiber.Ctx) string { return \"\" },\n\tUnauthorized:  func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusUnauthorized) },\n\tForbidden:     func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusForbidden) },\n}\n\n// Helper function to set default values\nfunc configDefault(config ...Config) (Config, error) {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn ConfigDefault, nil\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\tif cfg.Enforcer == nil {\n\t\tif cfg.ModelFilePath == \"\" {\n\t\t\tcfg.ModelFilePath = ConfigDefault.ModelFilePath\n\t\t}\n\n\t\tif cfg.PolicyAdapter == nil {\n\t\t\tcfg.PolicyAdapter = ConfigDefault.PolicyAdapter\n\t\t}\n\n\t\tenforcer, err := casbin.NewEnforcer(cfg.ModelFilePath, cfg.PolicyAdapter)\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\n\t\tcfg.Enforcer = enforcer\n\t}\n\n\tif cfg.Lookup == nil {\n\t\tcfg.Lookup = ConfigDefault.Lookup\n\t}\n\n\tif cfg.Unauthorized == nil {\n\t\tcfg.Unauthorized = ConfigDefault.Unauthorized\n\t}\n\n\tif cfg.Forbidden == nil {\n\t\tcfg.Forbidden = ConfigDefault.Forbidden\n\t}\n\n\treturn cfg, nil\n}\n"
  },
  {
    "path": "v3/casbin/go.mod",
    "content": "module github.com/gofiber/contrib/v3/casbin\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/casbin/casbin/v2 v2.135.0\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0 // indirect\n\tgithub.com/casbin/govaluate v1.10.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n)\n"
  },
  {
    "path": "v3/casbin/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=\ngithub.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=\ngithub.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=\ngithub.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=\ngithub.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\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": "v3/casbin/options.go",
    "content": "package casbin\n\nimport \"strings\"\n\nconst (\n\tMatchAllRule ValidationRule = iota\n\tAtLeastOneRule\n)\n\nvar OptionsDefault = Options{\n\tValidationRule:   MatchAllRule,\n\tPermissionParser: PermissionParserWithSeperator(\":\"),\n}\n\ntype (\n\tValidationRule int\n\t// PermissionParserFunc is used for parsing the permission\n\t// to extract object and action usually\n\tPermissionParserFunc func(str string) []string\n\tOptionFunc           func(*Options)\n\t// Option specifies casbin configuration options.\n\tOption interface {\n\t\tapply(*Options)\n\t}\n\t// Options holds Options of middleware\n\tOptions struct {\n\t\tValidationRule   ValidationRule\n\t\tPermissionParser PermissionParserFunc\n\t}\n)\n\nfunc (of OptionFunc) apply(o *Options) {\n\tof(o)\n}\n\nfunc WithValidationRule(vr ValidationRule) Option {\n\treturn OptionFunc(func(o *Options) {\n\t\to.ValidationRule = vr\n\t})\n}\n\nfunc WithPermissionParser(pp PermissionParserFunc) Option {\n\treturn OptionFunc(func(o *Options) {\n\t\to.PermissionParser = pp\n\t})\n}\n\nfunc PermissionParserWithSeperator(sep string) PermissionParserFunc {\n\treturn func(str string) []string {\n\t\treturn strings.Split(str, sep)\n\t}\n}\n\n// Helper function to set default values\nfunc optionsDefault(opts ...Option) Options {\n\tcfg := OptionsDefault\n\n\tfor _, opt := range opts {\n\t\topt.apply(&cfg)\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/casbin/utils.go",
    "content": "package casbin\n\nfunc containsString(s []string, v string) bool {\n\tfor _, vv := range s {\n\t\tif vv == v {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc stringSliceToInterfaceSlice(s []string) []interface{} {\n\tres := make([]interface{}, len(s))\n\tfor i, v := range s {\n\t\tres[i] = v\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "v3/circuitbreaker/README.md",
    "content": "---\nid: circuitbreaker\n---\n\n# Circuit Breaker\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*circuitbreaker*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20CircuitBreaker/badge.svg)\n\nA **Circuit Breaker** is a software design pattern used to prevent system failures when a service is experiencing high failures or slow responses. It helps improve system resilience by **stopping requests** to an unhealthy service and **allowing recovery** once it stabilizes.\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## How It Works\n\n1. **Closed State:**  \n   - Requests are allowed to pass normally.  \n   - Failures are counted.  \n   - If failures exceed a defined **threshold**, the circuit switches to **Open** state.  \n\n2. **Open State:**  \n   - Requests are **blocked immediately** to prevent overload.  \n   - The circuit stays open for a **timeout period** before moving to **Half-Open**.  \n\n3. **Half-Open State:**  \n   - Allows a limited number of requests to test service recovery.  \n   - If requests **succeed**, the circuit resets to **Closed**.  \n   - If requests **fail**, the circuit returns to **Open**.\n\n## Benefits of Using a Circuit Breaker\n\n✅ **Prevents cascading failures** in microservices.  \n✅ **Improves system reliability** by avoiding repeated failed requests.  \n✅ **Reduces load on struggling services** and allows recovery.  \n\n## Install\n\n```bash\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/circuitbreaker\n```\n\n## Signature\n\n```go\ncircuitbreaker.New(config ...circuitbreaker.Config) *circuitbreaker.Middleware \n```\n\n## Config\n\n| Property | Type | Description | Default |\n|:---------|:-----|:------------|:--------|\n| FailureThreshold | `int` | Number of consecutive errors required to open the circuit | `5` |\n| Timeout | `time.Duration` | Timeout for the circuit breaker | `10 * time.Second` |\n| SuccessThreshold | `int` | Number of successful requests required to close the circuit | `5` |\n| HalfOpenMaxConcurrent | `int` | Max concurrent requests in half-open state | `1` |\n| IsFailure | `func(error) bool` | Custom function to determine if an error is a failure | `Status >= 500` |\n| OnOpen | `func(fiber.Ctx) error` | Callback function when the circuit is opened | `503 response` |\n| OnClose | `func(fiber.Ctx) error` | Callback function when the circuit is closed | `Continue request` |\n| OnHalfOpen | `func(fiber.Ctx) error` | Callback function when the circuit is half-open | `429 response` |\n\n## Circuit Breaker Usage in Fiber (Example)\n\nThis guide explains how to use a Circuit Breaker in a Fiber application at different levels, from basic setup to advanced customization.\n\n### 1. Basic Setup\n\nA **global** Circuit Breaker protects all routes.\n\n**Example: Applying Circuit Breaker to All Routes**\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/contrib/v3/circuitbreaker\"\n)\n\nfunc main() {\n    app := fiber.New()\n    \n    // Create a new Circuit Breaker with custom configuration\n    cb := circuitbreaker.New(circuitbreaker.Config{\n        FailureThreshold: 3,               // Max failures before opening the circuit\n        Timeout:          5 * time.Second, // Wait time before retrying\n        SuccessThreshold: 2,               // Required successes to move back to closed state\n    })\n\n    // Apply Circuit Breaker to ALL routes\n    app.Use(circuitbreaker.Middleware(cb))\n\n    // Sample Route\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, world!\")\n    })\n\n    // Optional: Expose health check endpoint\n    app.Get(\"/health/circuit\", cb.HealthHandler())\n\n    // Optional: Expose metrics about the circuit breaker:\n    app.Get(\"/metrics/circuit\", func(c fiber.Ctx) error {\n          return c.JSON(cb.GetStateStats())\n    })\n\n    app.Listen(\":3000\")\n\n    // In your application shutdown logic\n    app.Shutdown(func() {\n        // Make sure to stop the circuit breaker when your application shuts down:\n        cb.Stop()\n    })\n}\n```\n\n### 2. Route & Route-Group Specific Circuit Breaker\n\nApply the Circuit Breaker **only to specific routes**.\n\n```go\napp.Get(\"/protected\", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error {\n    return c.SendString(\"Protected service running\")\n})\n```\nApply the Circuit Breaker **only to specific routes groups**.\n\n```go\napp := route.Group(\"/api\")\napp.Use(circuitbreaker.Middleware(cb))\n\n// All routes in this group will be protected\napp.Get(\"/users\", getUsersHandler)\napp.Post(\"/users\", createUserHandler)\n```\n\n### 3. Circuit Breaker with Custom Failure Handling\n\nCustomize the response when the circuit **opens**.\n\n```go\ncb := circuitbreaker.New(circuitbreaker.Config{\n    FailureThreshold: 3,\n    Timeout:   10 * time.Second,\n    OnOpen: func(c fiber.Ctx) error {\n        return c.Status(fiber.StatusServiceUnavailable).\n            JSON(fiber.Map{\"error\": \"Circuit Open: Service unavailable\"})\n    },\n    OnHalfOpen: func(c fiber.Ctx) error {\n        return c.Status(fiber.StatusTooManyRequests).\n            JSON(fiber.Map{\"error\": \"Circuit Half-Open: Retrying service\"})\n    },\n    OnClose: func(c fiber.Ctx) error {\n        return c.Status(fiber.StatusOK).\n            JSON(fiber.Map{\"message\": \"Circuit Closed: Service recovered\"})\n    },\n})\n\n// Apply to a specific route\napp.Get(\"/custom\", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error {\n    return c.SendString(\"This service is protected by a Circuit Breaker\")\n})\n```\n\n✅ Now, when failures exceed the threshold, ***custom error responses** will be sent.\n\n### 4. Circuit Breaker for External API Calls\n\nUse a Circuit Breaker **when calling an external API.**\n\n```go\n\napp.Get(\"/external-api\", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error {\n    // Simulating an external API call\n    resp, err := fiber.Get(\"https://example.com/api\")\n    if err != nil {\n        return fiber.NewError(fiber.StatusInternalServerError, \"External API failed\")\n    }\n    return c.SendString(resp.Body())\n})\n```\n\n✅ If the external API fails repeatedly, **the circuit breaker prevents further calls.**\n\n### 5. Circuit Breaker with Concurrent Requests Handling\n\nUse a **semaphore-based** approach to **limit concurrent requests.**\n\n```go\ncb := circuitbreaker.New(circuitbreaker.Config{\n    FailureThreshold:  3,\n    Timeout:           5 * time.Second,\n    SuccessThreshold:  2,\n    HalfOpenSemaphore: make(chan struct{}, 2), // Allow only 2 concurrent requests\n})\n\napp.Get(\"/half-open-limit\", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error {\n    time.Sleep(2 * time.Second) // Simulating slow response\n    return c.SendString(\"Half-Open: Limited concurrent requests\")\n})\n```\n\n✅ When in **half-open** state, only **2 concurrent requests are allowed**.\n\n### 6. Circuit Breaker with Custom Metrics\n\nIntegrate **Prometheus metrics** and **structured logging**.\n\n```go\ncb := circuitbreaker.New(circuitbreaker.Config{\n    FailureThreshold: 5,\n    Timeout:   10 * time.Second,\n    OnOpen: func(c fiber.Ctx) error {\n        log.Println(\"Circuit Breaker Opened!\")\n        prometheus.Inc(\"circuit_breaker_open_count\")\n        return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{\"error\": \"Service Down\"})\n    },\n})\n```\n\n✅ Logs when the circuit opens & increments Prometheus metrics.\n\n### 7. Advanced: Multiple Circuit Breakers for Different Services\n\nUse different Circuit Breakers for different services.\n\n```go\n\ndbCB := circuitbreaker.New(circuitbreaker.Config{FailureThreshold: 5, Timeout: 10 * time.Second})\napiCB := circuitbreaker.New(circuitbreaker.Config{FailureThreshold: 3, Timeout: 5 * time.Second})\n\napp.Get(\"/db-service\", circuitbreaker.Middleware(dbCB), func(c fiber.Ctx) error {\n    return c.SendString(\"DB service request\")\n})\n\napp.Get(\"/api-service\", circuitbreaker.Middleware(apiCB), func(c fiber.Ctx) error {\n    return c.SendString(\"External API service request\")\n})\n```\n\n✅ Each service has its own failure threshold & timeout.\n"
  },
  {
    "path": "v3/circuitbreaker/circuitbreaker.go",
    "content": "package circuitbreaker\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// State represents the state of the circuit breaker\ntype State string\n\nconst (\n\tStateClosed   State = \"closed\"    // Normal operation\n\tStateOpen     State = \"open\"      // Requests are blocked\n\tStateHalfOpen State = \"half-open\" // Limited requests allowed to check recovery\n)\n\n// Config holds the configurable parameters\ntype Config struct {\n\t// Failure threshold to trip the circuit\n\tFailureThreshold int\n\t// Duration circuit stays open before allowing test requests\n\tTimeout time.Duration\n\t// Success threshold to close the circuit from half-open\n\tSuccessThreshold int\n\t// Maximum concurrent requests allowed in half-open state\n\tHalfOpenMaxConcurrent int\n\t// Custom failure detector function (return true if response should count as failure)\n\tIsFailure func(c fiber.Ctx, err error) bool\n\t// Callbacks for state transitions\n\tOnOpen     func(fiber.Ctx) error // Called when circuit opens\n\tOnHalfOpen func(fiber.Ctx) error // Called when circuit transitions to half-open\n\tOnClose    func(fiber.Ctx) error // Called when circuit closes\n}\n\n// DefaultConfig provides sensible defaults for the circuit breaker\nvar DefaultConfig = Config{\n\tFailureThreshold:      5,\n\tTimeout:               5 * time.Second,\n\tSuccessThreshold:      1,\n\tHalfOpenMaxConcurrent: 1,\n\tIsFailure: func(c fiber.Ctx, err error) bool {\n\t\treturn err != nil || c.Response().StatusCode() >= http.StatusInternalServerError\n\t},\n\tOnOpen: func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{\n\t\t\t\"error\": \"service unavailable\",\n\t\t})\n\t},\n\tOnHalfOpen: func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{\n\t\t\t\"error\": \"service under recovery\",\n\t\t})\n\t},\n\tOnClose: func(c fiber.Ctx) error {\n\t\treturn c.Next()\n\t},\n}\n\n// CircuitBreaker implements the circuit breaker pattern\ntype CircuitBreaker struct {\n\tfailureCount      int64              // Count of failures (atomic)\n\tsuccessCount      int64              // Count of successes in half-open state (atomic)\n\ttotalRequests     int64              // Count of total requests (atomic)\n\trejectedRequests  int64              // Count of rejected requests (atomic)\n\tstate             State              // Current state of circuit breaker\n\tmutex             sync.RWMutex       // Protects state transitions\n\tfailureThreshold  int                // Max failures before opening circuit\n\ttimeout           time.Duration      // Duration to stay open before transitioning to half-open\n\tsuccessThreshold  int                // Successes required to close circuit\n\topenTimer         *time.Timer        // Timer for state transition from open to half-open\n\tctx               context.Context    // Context for cancellation\n\tcancel            context.CancelFunc // Cancel function for cleanup\n\tconfig            Config             // Configuration settings\n\tnow               func() time.Time   // Function for getting current time (useful for testing)\n\thalfOpenSemaphore chan struct{}      // Controls limited requests in half-open state\n\tlastStateChange   time.Time          // Time of last state change\n}\n\n// New initializes a circuit breaker with the given configuration\nfunc New(config Config) *CircuitBreaker {\n\t// Apply default values for zero values\n\tif config.FailureThreshold <= 0 {\n\t\tconfig.FailureThreshold = DefaultConfig.FailureThreshold\n\t}\n\tif config.Timeout <= 0 {\n\t\tconfig.Timeout = DefaultConfig.Timeout\n\t}\n\tif config.SuccessThreshold <= 0 {\n\t\tconfig.SuccessThreshold = DefaultConfig.SuccessThreshold\n\t}\n\tif config.HalfOpenMaxConcurrent <= 0 {\n\t\tconfig.HalfOpenMaxConcurrent = DefaultConfig.HalfOpenMaxConcurrent\n\t}\n\tif config.IsFailure == nil {\n\t\tconfig.IsFailure = DefaultConfig.IsFailure\n\t}\n\tif config.OnOpen == nil {\n\t\tconfig.OnOpen = DefaultConfig.OnOpen\n\t}\n\tif config.OnHalfOpen == nil {\n\t\tconfig.OnHalfOpen = DefaultConfig.OnHalfOpen\n\t}\n\tif config.OnClose == nil {\n\t\tconfig.OnClose = DefaultConfig.OnClose\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tnow := time.Now()\n\n\treturn &CircuitBreaker{\n\t\tfailureThreshold:  config.FailureThreshold,\n\t\ttimeout:           config.Timeout,\n\t\tsuccessThreshold:  config.SuccessThreshold,\n\t\tstate:             StateClosed,\n\t\tctx:               ctx,\n\t\tcancel:            cancel,\n\t\tconfig:            config,\n\t\tnow:               time.Now,\n\t\thalfOpenSemaphore: make(chan struct{}, config.HalfOpenMaxConcurrent),\n\t\tlastStateChange:   now,\n\t\ttotalRequests:     0,\n\t\trejectedRequests:  0,\n\t}\n}\n\n// Stop cancels the circuit breaker and releases resources\nfunc (cb *CircuitBreaker) Stop() {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\tif cb.openTimer != nil {\n\t\tcb.openTimer.Stop()\n\t}\n\tcb.cancel()\n}\n\n// GetState returns the current state of the circuit breaker\nfunc (cb *CircuitBreaker) GetState() State {\n\tcb.mutex.RLock()\n\tdefer cb.mutex.RUnlock()\n\treturn cb.state\n}\n\n// IsOpen returns true if the circuit is open\nfunc (cb *CircuitBreaker) IsOpen() bool {\n\treturn cb.GetState() == StateOpen\n}\n\n// Reset resets the circuit breaker to its initial closed state\nfunc (cb *CircuitBreaker) Reset() {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\t// Reset counters\n\tatomic.StoreInt64(&cb.failureCount, 0)\n\tatomic.StoreInt64(&cb.successCount, 0)\n\n\t// Reset state\n\tcb.state = StateClosed\n\tcb.lastStateChange = cb.now()\n\n\t// Cancel any pending state transitions\n\tif cb.openTimer != nil {\n\t\tcb.openTimer.Stop()\n\t}\n}\n\n// ForceOpen forcibly opens the circuit regardless of failure count\nfunc (cb *CircuitBreaker) ForceOpen() {\n\tcb.transitionToOpen()\n}\n\n// ForceClose forcibly closes the circuit regardless of current state\nfunc (cb *CircuitBreaker) ForceClose() {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\tcb.state = StateClosed\n\tcb.lastStateChange = cb.now()\n\tatomic.StoreInt64(&cb.failureCount, 0)\n\tatomic.StoreInt64(&cb.successCount, 0)\n\n\tif cb.openTimer != nil {\n\t\tcb.openTimer.Stop()\n\t}\n}\n\n// SetTimeout updates the timeout duration\nfunc (cb *CircuitBreaker) SetTimeout(timeout time.Duration) {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\tcb.timeout = timeout\n}\n\n// transitionToOpen changes state to open and schedules transition to half-open\nfunc (cb *CircuitBreaker) transitionToOpen() {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\tif cb.state != StateOpen {\n\t\tcb.state = StateOpen\n\t\tcb.lastStateChange = cb.now()\n\n\t\t// Stop existing timer if any\n\t\tif cb.openTimer != nil {\n\t\t\tcb.openTimer.Stop()\n\t\t}\n\n\t\t// Schedule transition to half-open after timeout\n\t\tcb.openTimer = time.AfterFunc(cb.timeout, func() {\n\t\t\tcb.transitionToHalfOpen()\n\t\t})\n\n\t\t// Reset failure counter\n\t\tatomic.StoreInt64(&cb.failureCount, 0)\n\t}\n}\n\n// transitionToHalfOpen changes state from open to half-open\nfunc (cb *CircuitBreaker) transitionToHalfOpen() {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\tif cb.state == StateOpen {\n\t\tcb.state = StateHalfOpen\n\t\tcb.lastStateChange = cb.now()\n\n\t\t// Reset counters\n\t\tatomic.StoreInt64(&cb.failureCount, 0)\n\t\tatomic.StoreInt64(&cb.successCount, 0)\n\n\t\t// Empty the semaphore channel\n\t\tselect {\n\t\tcase <-cb.halfOpenSemaphore:\n\t\tdefault:\n\t\t}\n\t}\n}\n\n// transitionToClosed changes state from half-open to closed\nfunc (cb *CircuitBreaker) transitionToClosed() {\n\tcb.mutex.Lock()\n\tdefer cb.mutex.Unlock()\n\n\tif cb.state == StateHalfOpen {\n\t\tcb.state = StateClosed\n\t\tcb.lastStateChange = cb.now()\n\n\t\t// Reset counters\n\t\tatomic.StoreInt64(&cb.failureCount, 0)\n\t\tatomic.StoreInt64(&cb.successCount, 0)\n\t}\n}\n\n// AllowRequest determines if a request is allowed based on circuit state\nfunc (cb *CircuitBreaker) AllowRequest() (bool, State) {\n\tatomic.AddInt64(&cb.totalRequests, 1)\n\n\tcb.mutex.RLock()\n\tstate := cb.state\n\tcb.mutex.RUnlock()\n\n\tswitch state {\n\tcase StateOpen:\n\t\tatomic.AddInt64(&cb.rejectedRequests, 1)\n\t\treturn false, state\n\tcase StateHalfOpen:\n\t\tselect {\n\t\tcase cb.halfOpenSemaphore <- struct{}{}:\n\t\t\treturn true, state\n\t\tdefault:\n\t\t\tatomic.AddInt64(&cb.rejectedRequests, 1)\n\t\t\treturn false, state\n\t\t}\n\tdefault: // StateClosed\n\t\treturn true, state\n\t}\n}\n\n// ReleaseSemaphore releases a slot in the half-open semaphore\nfunc (cb *CircuitBreaker) ReleaseSemaphore() {\n\tselect {\n\tcase <-cb.halfOpenSemaphore:\n\tdefault:\n\t}\n}\n\n// ReportSuccess increments success count and closes circuit if threshold met\nfunc (cb *CircuitBreaker) ReportSuccess() {\n\tcb.mutex.RLock()\n\tcurrentState := cb.state\n\tcb.mutex.RUnlock()\n\n\tif currentState == StateHalfOpen {\n\t\tnewSuccessCount := atomic.AddInt64(&cb.successCount, 1)\n\t\tif int(newSuccessCount) >= cb.successThreshold {\n\t\t\tcb.transitionToClosed()\n\t\t}\n\t}\n}\n\n// ReportFailure increments failure count and opens circuit if threshold met\nfunc (cb *CircuitBreaker) ReportFailure() {\n\tcb.mutex.RLock()\n\tcurrentState := cb.state\n\tcb.mutex.RUnlock()\n\n\tswitch currentState {\n\tcase StateHalfOpen:\n\t\t// In half-open, a single failure trips the circuit\n\t\tcb.transitionToOpen()\n\tcase StateClosed:\n\t\tnewFailureCount := atomic.AddInt64(&cb.failureCount, 1)\n\t\tif int(newFailureCount) >= cb.failureThreshold {\n\t\t\tcb.transitionToOpen()\n\t\t}\n\t}\n}\n\n// Metrics returns basic metrics about the circuit breaker\nfunc (cb *CircuitBreaker) Metrics() fiber.Map {\n\treturn fiber.Map{\n\t\t\"state\":            cb.GetState(),\n\t\t\"failures\":         atomic.LoadInt64(&cb.failureCount),\n\t\t\"successes\":        atomic.LoadInt64(&cb.successCount),\n\t\t\"totalRequests\":    atomic.LoadInt64(&cb.totalRequests),\n\t\t\"rejectedRequests\": atomic.LoadInt64(&cb.rejectedRequests),\n\t}\n}\n\n// GetStateStats returns detailed statistics about the circuit breaker\nfunc (cb *CircuitBreaker) GetStateStats() fiber.Map {\n\tstate := cb.GetState()\n\n\treturn fiber.Map{\n\t\t\"state\":            state,\n\t\t\"failures\":         atomic.LoadInt64(&cb.failureCount),\n\t\t\"successes\":        atomic.LoadInt64(&cb.successCount),\n\t\t\"totalRequests\":    atomic.LoadInt64(&cb.totalRequests),\n\t\t\"rejectedRequests\": atomic.LoadInt64(&cb.rejectedRequests),\n\t\t\"lastStateChange\":  cb.lastStateChange,\n\t\t\"openDuration\":     cb.timeout,\n\t\t\"failureThreshold\": cb.failureThreshold,\n\t\t\"successThreshold\": cb.successThreshold,\n\t}\n}\n\n// HealthHandler returns a Fiber handler for checking circuit breaker status\nfunc (cb *CircuitBreaker) HealthHandler() fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\tstate := cb.GetState()\n\n\t\tdata := fiber.Map{\n\t\t\t\"state\":   state,\n\t\t\t\"healthy\": state == StateClosed,\n\t\t}\n\n\t\tif state == StateOpen {\n\t\t\treturn c.Status(fiber.StatusServiceUnavailable).JSON(data)\n\t\t}\n\n\t\treturn c.JSON(data)\n\t}\n}\n\n// Middleware wraps the fiber handler with circuit breaker logic\nfunc Middleware(cb *CircuitBreaker) fiber.Handler {\n\treturn func(c fiber.Ctx) error {\n\t\tallowed, state := cb.AllowRequest()\n\n\t\tif !allowed {\n\t\t\t// Call appropriate callback based on state\n\t\t\tif state == StateHalfOpen && cb.config.OnHalfOpen != nil {\n\t\t\t\treturn cb.config.OnHalfOpen(c)\n\t\t\t} else if state == StateOpen && cb.config.OnOpen != nil {\n\t\t\t\treturn cb.config.OnOpen(c)\n\t\t\t}\n\t\t\treturn c.SendStatus(fiber.StatusServiceUnavailable)\n\t\t}\n\n\t\t// If request allowed in half-open state, ensure semaphore is released\n\t\thalfOpen := state == StateHalfOpen\n\t\tif halfOpen {\n\t\t\tdefer cb.ReleaseSemaphore()\n\t\t}\n\n\t\t// Execute the request\n\t\terr := c.Next()\n\n\t\t// Check if the response should be considered a failure\n\t\tif cb.config.IsFailure(c, err) {\n\t\t\tcb.ReportFailure()\n\t\t} else {\n\t\t\tcb.ReportSuccess()\n\n\t\t\t// If transition to closed state just happened, trigger callback\n\t\t\tif halfOpen && cb.GetState() == StateClosed && cb.config.OnClose != nil {\n\t\t\t\t// We don't return this error as it would override the actual response\n\t\t\t\t_ = cb.config.OnClose(c)\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "v3/circuitbreaker/circuitbreaker_test.go",
    "content": "package circuitbreaker\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http/httptest\"\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/stretchr/testify/require\"\n)\n\n// mockTime helps control time for deterministic testing\ntype mockTime struct {\n\tmu      sync.Mutex\n\tcurrent time.Time\n}\n\nfunc newMockTime(t time.Time) *mockTime {\n\treturn &mockTime{current: t}\n}\n\nfunc (m *mockTime) Now() time.Time {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\treturn m.current\n}\n\nfunc (m *mockTime) Add(d time.Duration) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.current = m.current.Add(d)\n}\n\n// TestCircuitBreakerStates tests each state transition of the circuit breaker\nfunc TestCircuitBreakerStates(t *testing.T) {\n\tmockClock := newMockTime(time.Now())\n\n\t// Create circuit breaker with test config\n\tcb := New(Config{\n\t\tFailureThreshold:      2,\n\t\tTimeout:               5 * time.Second,\n\t\tSuccessThreshold:      2,\n\t\tHalfOpenMaxConcurrent: 1,\n\t})\n\n\t// Override the time function\n\tcb.now = mockClock.Now\n\n\t// Test initial state\n\tt.Run(\"Initial State\", func(t *testing.T) {\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t\tallowed, state := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\t\trequire.Equal(t, StateClosed, state)\n\t})\n\n\t// Test transition to open state\n\tt.Run(\"Transition to Open\", func(t *testing.T) {\n\t\t// Report failures to trip the circuit\n\t\tcb.ReportFailure()\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\n\t\tcb.ReportFailure() // This should trip the circuit\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\tallowed, state := cb.AllowRequest()\n\t\trequire.False(t, allowed)\n\t\trequire.Equal(t, StateOpen, state)\n\t})\n\n\t// Test transition to half-open state\n\tt.Run(\"Transition to HalfOpen\", func(t *testing.T) {\n\t\t// Advance time past the timeout to trigger half-open\n\t\tmockClock.Add(6 * time.Second)\n\n\t\t// Force timer activation by checking state\n\t\t// (In real usage this would happen automatically with timer)\n\t\tif cb.openTimer != nil {\n\t\t\tcb.openTimer.Stop()\n\t\t\tcb.transitionToHalfOpen()\n\t\t}\n\n\t\trequire.Equal(t, StateHalfOpen, cb.GetState())\n\n\t\tallowed, state := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\t\trequire.Equal(t, StateHalfOpen, state)\n\n\t\t// Release the semaphore for next test\n\t\tcb.ReleaseSemaphore()\n\t})\n\n\t// Test half-open limited concurrency\n\tt.Run(\"HalfOpen Limited Concurrency\", func(t *testing.T) {\n\t\t// Try to allow two concurrent requests when only one is permitted\n\t\tallowed1, _ := cb.AllowRequest()\n\t\tallowed2, _ := cb.AllowRequest()\n\n\t\trequire.True(t, allowed1)\n\t\trequire.False(t, allowed2)\n\n\t\t// Release the semaphore\n\t\tcb.ReleaseSemaphore()\n\t})\n\n\t// Test transition back to open on failure in half-open\n\tt.Run(\"HalfOpen to Open on Failure\", func(t *testing.T) {\n\t\tallowed, _ := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\n\t\tcb.ReportFailure()\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\t// Even though we took a semaphore, it should be cleared by state transition\n\t\tallowed, _ = cb.AllowRequest()\n\t\trequire.False(t, allowed)\n\t})\n\n\t// Test transition to half-open again\n\tt.Run(\"Back to HalfOpen\", func(t *testing.T) {\n\t\tmockClock.Add(6 * time.Second)\n\n\t\t// Force timer activation\n\t\tif cb.openTimer != nil {\n\t\t\tcb.openTimer.Stop()\n\t\t\tcb.transitionToHalfOpen()\n\t\t}\n\n\t\trequire.Equal(t, StateHalfOpen, cb.GetState())\n\t})\n\n\t// Test transition to closed state\n\tt.Run(\"Transition to Closed\", func(t *testing.T) {\n\t\tallowed, _ := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\n\t\tcb.ReportSuccess()\n\t\trequire.Equal(t, StateHalfOpen, cb.GetState())\n\n\t\tcb.ReleaseSemaphore()\n\t\tallowed, _ = cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\n\t\tcb.ReportSuccess() // This should close the circuit\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\n\t\tcb.ReleaseSemaphore()\n\t})\n\n\t// Test proper cleanup\n\tt.Run(\"Cleanup\", func(t *testing.T) {\n\t\tcb.Stop()\n\t})\n}\n\n// TestCircuitBreakerCallbacks tests the callback functions\nfunc TestCircuitBreakerCallbacks(t *testing.T) {\n\tvar (\n\t\topenCalled     bool\n\t\thalfOpenCalled bool\n\t\tclosedCalled   bool\n\t)\n\n\tcb := New(Config{\n\t\tFailureThreshold:      2,\n\t\tTimeout:               1 * time.Millisecond, // Short timeout for quick tests\n\t\tSuccessThreshold:      1,\n\t\tHalfOpenMaxConcurrent: 1,\n\t\tOnOpen: func(c fiber.Ctx) error {\n\t\t\topenCalled = true\n\t\t\treturn c.SendStatus(fiber.StatusServiceUnavailable)\n\t\t},\n\t\tOnHalfOpen: func(c fiber.Ctx) error {\n\t\t\thalfOpenCalled = true\n\t\t\treturn c.SendStatus(fiber.StatusTooManyRequests)\n\t\t},\n\t\tOnClose: func(c fiber.Ctx) error {\n\t\t\tclosedCalled = true\n\t\t\treturn c.Next()\n\t\t},\n\t})\n\n\tapp := fiber.New()\n\n\tapp.Use(Middleware(cb))\n\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\t// Test OnOpen callback\n\tt.Run(\"OnOpen Callback\", func(t *testing.T) {\n\t\t// Trip the circuit\n\t\tcb.ReportFailure()\n\t\tcb.ReportFailure()\n\n\t\t// Request should be rejected\n\t\treq := httptest.NewRequest(\"GET\", \"/test\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)\n\t\trequire.True(t, openCalled)\n\t})\n\n\t// Test OnHalfOpen callback\n\tt.Run(\"OnHalfOpen Callback\", func(t *testing.T) {\n\t\tcb.transitionToHalfOpen()\n\n\t\t// Acquire the one allowed request\n\t\tallowed, state := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\t\trequire.Equal(t, StateHalfOpen, state)\n\n\t\t// Second request should be rejected with OnHalfOpen callback\n\t\treq := httptest.NewRequest(\"GET\", \"/test\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n\t\trequire.True(t, halfOpenCalled)\n\n\t\t// Release the semaphore\n\t\tcb.ReleaseSemaphore()\n\t})\n\n\t// Test OnClose callback\n\tt.Run(\"OnClose Callback\", func(t *testing.T) {\n\t\t// Reset for clean test\n\t\tclosedCalled = false\n\n\t\t// Get to half-open state\n\t\tcb.transitionToHalfOpen()\n\n\t\t// Create a test request\n\t\treq := httptest.NewRequest(\"GET\", \"/test\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t\trequire.True(t, closedCalled) // OnClose should be called after successful request\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestMiddleware tests the middleware functionality\nfunc TestMiddleware(t *testing.T) {\n\tcustomErr := errors.New(\"custom error\")\n\n\tcb := New(Config{\n\t\tFailureThreshold: 2,\n\t\tTimeout:          5 * time.Second,\n\t\tSuccessThreshold: 2,\n\t\tIsFailure: func(c fiber.Ctx, err error) bool {\n\t\t\t// Count as failure if status >= 400 or has error\n\t\t\treturn err != nil || c.Response().StatusCode() >= 400\n\t\t},\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Middleware(cb))\n\n\t// Success handler\n\tapp.Get(\"/success\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\t// Client error handler - 400 series\n\tapp.Get(\"/client-error\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusBadRequest)\n\t})\n\n\t// Server error handler - 500 series\n\tapp.Get(\"/server-error\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusInternalServerError)\n\t})\n\n\t// Error handler\n\tapp.Get(\"/error\", func(c fiber.Ctx) error {\n\t\treturn customErr\n\t})\n\n\tt.Run(\"Successful Request\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(\"GET\", \"/success\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t})\n\n\tt.Run(\"Client Error Counts as Failure\", func(t *testing.T) {\n\t\t// Reset to closed state\n\t\tcb.transitionToClosed()\n\n\t\t// Send client error requests\n\t\treq := httptest.NewRequest(\"GET\", \"/client-error\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\n\t\t// Should increment failure count - check state remains closed\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\n\t\t// Second failure should trip circuit\n\t\tresp, err = app.Test(req)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\n\t\t// Circuit should now be open\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\t})\n\n\tt.Run(\"Circuit Open Rejects Requests\", func(t *testing.T) {\n\t\t// Circuit should be open from previous test\n\t\treq := httptest.NewRequest(\"GET\", \"/success\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestConcurrentAccess tests the circuit breaker under concurrent load\nfunc TestConcurrentAccess(t *testing.T) {\n\tcb := New(Config{\n\t\tFailureThreshold:      5,\n\t\tTimeout:               100 * time.Millisecond,\n\t\tSuccessThreshold:      3,\n\t\tHalfOpenMaxConcurrent: 2,\n\t})\n\n\tt.Run(\"Concurrent Failures\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\n\t\t// Simulate 10 goroutines reporting failures\n\t\tfor i := 0; i < 10; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tcb.ReportFailure()\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Circuit should be open after enough failures\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\t})\n\n\tt.Run(\"Concurrent Half-Open Requests\", func(t *testing.T) {\n\t\t// Force transition to half-open\n\t\tcb.transitionToHalfOpen()\n\n\t\tvar wg sync.WaitGroup\n\t\trequestAllowed := make(chan bool, 10)\n\n\t\t// Try 10 concurrent requests\n\t\tfor i := 0; i < 10; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tallowed, _ := cb.AllowRequest()\n\t\t\t\trequestAllowed <- allowed\n\n\t\t\t\tif allowed {\n\t\t\t\t\t// Simulate request processing\n\t\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t\t\tcb.ReleaseSemaphore()\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\t\tclose(requestAllowed)\n\n\t\t// Count allowed requests\n\t\tallowedCount := 0\n\t\tfor allowed := range requestAllowed {\n\t\t\tif allowed {\n\t\t\t\tallowedCount++\n\t\t\t}\n\t\t}\n\n\t\t// Only HalfOpenMaxConcurrent (2) requests should be allowed\n\t\trequire.Equal(t, cb.config.HalfOpenMaxConcurrent, allowedCount)\n\t})\n\n\tt.Run(\"Concurrent Successes to Close Circuit\", func(t *testing.T) {\n\t\t// Force transition to half-open\n\t\tcb.transitionToHalfOpen()\n\n\t\tvar wg sync.WaitGroup\n\n\t\t// Simulate 10 goroutines reporting successes\n\t\tfor i := 0; i < 10; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tcb.ReportSuccess()\n\t\t\t}()\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Circuit should be closed after enough successes\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestCustomFailureDetection tests the custom failure detection logic\nfunc TestCustomFailureDetection(t *testing.T) {\n\tcustomFailureDetection := false\n\n\tcb := New(Config{\n\t\tFailureThreshold: 1,\n\t\tIsFailure: func(c fiber.Ctx, err error) bool {\n\t\t\t// Custom logic: mark as failure only if our flag is set\n\t\t\treturn customFailureDetection\n\t\t},\n\t})\n\n\tapp := fiber.New()\n\tapp.Use(Middleware(cb))\n\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tt.Run(\"Custom Success Logic\", func(t *testing.T) {\n\t\tcustomFailureDetection = false\n\n\t\t// Even 500 status should be success with our custom logic\n\t\tapp.Get(\"/server-error\", func(c fiber.Ctx) error {\n\t\t\tc.Status(500)\n\t\t\treturn nil\n\t\t})\n\n\t\treq := httptest.NewRequest(\"GET\", \"/server-error\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 500, resp.StatusCode)\n\n\t\t// Circuit should remain closed\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t})\n\n\tt.Run(\"Custom Failure Logic\", func(t *testing.T) {\n\t\tcustomFailureDetection = true\n\n\t\t// Now even 200 status should be failure with our custom logic\n\t\treq := httptest.NewRequest(\"GET\", \"/test\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\t// Circuit should be open\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestHalfOpenConcurrencyConfig tests that HalfOpenMaxConcurrent setting works\nfunc TestHalfOpenConcurrencyConfig(t *testing.T) {\n\t// Create circuit breaker with 3 concurrent requests in half-open\n\tcb := New(Config{\n\t\tFailureThreshold:      2,\n\t\tTimeout:               5 * time.Second,\n\t\tSuccessThreshold:      2,\n\t\tHalfOpenMaxConcurrent: 3,\n\t})\n\n\t// Put circuit in half-open state\n\tcb.transitionToOpen()\n\tcb.transitionToHalfOpen()\n\n\t// Try to get more than allowed concurrent requests\n\tallowed1, _ := cb.AllowRequest()\n\tallowed2, _ := cb.AllowRequest()\n\tallowed3, _ := cb.AllowRequest()\n\tallowed4, _ := cb.AllowRequest()\n\n\trequire.True(t, allowed1)\n\trequire.True(t, allowed2)\n\trequire.True(t, allowed3)\n\trequire.False(t, allowed4)\n\n\t// Release all permits\n\tcb.ReleaseSemaphore()\n\tcb.ReleaseSemaphore()\n\tcb.ReleaseSemaphore()\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestCircuitBreakerReset tests the Reset method\nfunc TestCircuitBreakerReset(t *testing.T) {\n\tmockClock := newMockTime(time.Now())\n\n\tcb := New(Config{\n\t\tFailureThreshold:      2,\n\t\tTimeout:               5 * time.Second,\n\t\tSuccessThreshold:      2,\n\t\tHalfOpenMaxConcurrent: 1,\n\t})\n\tcb.now = mockClock.Now\n\n\tt.Run(\"Reset From Open State\", func(t *testing.T) {\n\t\t// Put circuit in open state\n\t\tcb.ReportFailure()\n\t\tcb.ReportFailure()\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\t// Reset the circuit\n\t\tcb.Reset()\n\n\t\t// Verify state and counters\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t\trequire.Equal(t, int64(0), atomic.LoadInt64(&cb.failureCount))\n\t\trequire.Equal(t, int64(0), atomic.LoadInt64(&cb.successCount))\n\t})\n\n\tt.Run(\"Reset From HalfOpen State\", func(t *testing.T) {\n\t\t// Put circuit in half-open state\n\t\tcb.ReportFailure()\n\t\tcb.ReportFailure()\n\t\tcb.transitionToHalfOpen()\n\t\trequire.Equal(t, StateHalfOpen, cb.GetState())\n\n\t\t// Take a semaphore\n\t\tallowed, _ := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\n\t\t// Reset the circuit\n\t\tcb.Reset()\n\n\t\t// Verify state and that new requests are allowed\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t\tallowed, _ = cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\t})\n\n\tt.Run(\"Reset With Active Timer\", func(t *testing.T) {\n\t\t// Put circuit in open state with active timer\n\t\tcb.ReportFailure()\n\t\tcb.ReportFailure()\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\t// Reset before timer expires\n\t\tcb.Reset()\n\n\t\t// Advance time past original timeout\n\t\tmockClock.Add(6 * time.Second)\n\n\t\t// Verify circuit remains closed\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t})\n\n\tt.Run(\"Reset Updates LastStateChange\", func(t *testing.T) {\n\t\tinitialTime := cb.lastStateChange\n\n\t\t// Wait a moment\n\t\tmockClock.Add(1 * time.Second)\n\n\t\t// Reset the circuit\n\t\tcb.Reset()\n\n\t\t// Verify lastStateChange was updated\n\t\trequire.True(t, cb.lastStateChange.After(initialTime))\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestCircuitBreakerForceOpen tests the ForceOpen method\nfunc TestForceOpen(t *testing.T) {\n\tmockClock := newMockTime(time.Now())\n\n\tcb := New(Config{\n\t\tFailureThreshold:      2,\n\t\tTimeout:               5 * time.Second,\n\t\tSuccessThreshold:      2,\n\t\tHalfOpenMaxConcurrent: 1,\n\t})\n\tcb.now = mockClock.Now\n\n\tt.Run(\"Force Open From Closed State\", func(t *testing.T) {\n\t\trequire.Equal(t, StateClosed, cb.GetState())\n\t\tcb.ForceOpen()\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\t// Verify requests are rejected\n\t\tallowed, state := cb.AllowRequest()\n\t\trequire.False(t, allowed)\n\t\trequire.Equal(t, StateOpen, state)\n\t})\n\n\tt.Run(\"Force Open From HalfOpen State\", func(t *testing.T) {\n\t\t// First get to half-open state\n\t\tcb.transitionToOpen()\n\t\tcb.transitionToHalfOpen()\n\n\t\trequire.Equal(t, StateHalfOpen, cb.GetState())\n\n\t\t// Take a semaphore\n\t\tallowed, _ := cb.AllowRequest()\n\t\trequire.True(t, allowed)\n\n\t\t// Force open should clear semaphore\n\t\tcb.ForceOpen()\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\t// Verify new requests are rejected\n\t\tallowed, _ = cb.AllowRequest()\n\t\trequire.False(t, allowed)\n\t})\n\n\tt.Run(\"Force Open With Active Timer\", func(t *testing.T) {\n\t\tcb.transitionToClosed()\n\t\tcb.ForceOpen()\n\n\t\t// Advance time past timeout\n\t\tmockClock.Add(6 * time.Second)\n\n\t\t// Should still be open since ForceOpen overrides normal timeout\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\t})\n\n\tt.Run(\"Force Open Multiple Times\", func(t *testing.T) {\n\t\t// Multiple force open calls should maintain open state\n\t\tcb.ForceOpen()\n\t\tcb.ForceOpen()\n\t\trequire.Equal(t, StateOpen, cb.GetState())\n\n\t\t// Verify counters are reset each time\n\t\trequire.Equal(t, int64(0), atomic.LoadInt64(&cb.failureCount))\n\t\trequire.Equal(t, int64(0), atomic.LoadInt64(&cb.successCount))\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n\n// TestHealthHandler tests the health check endpoint handler\nfunc TestHealthHandler(t *testing.T) {\n\tcb := New(Config{\n\t\tFailureThreshold:      2,\n\t\tTimeout:               5 * time.Second,\n\t\tSuccessThreshold:      2,\n\t\tHalfOpenMaxConcurrent: 1,\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/health\", cb.HealthHandler())\n\n\tt.Run(\"Healthy When Closed\", func(t *testing.T) {\n\t\tcb.transitionToClosed()\n\n\t\treq := httptest.NewRequest(\"GET\", \"/health\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\t\tvar result fiber.Map\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\terr = json.Unmarshal(body, &result)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, string(StateClosed), result[\"state\"])\n\t\trequire.Equal(t, true, result[\"healthy\"])\n\t})\n\n\tt.Run(\"Unhealthy When Open\", func(t *testing.T) {\n\t\tcb.transitionToOpen()\n\n\t\treq := httptest.NewRequest(\"GET\", \"/health\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)\n\n\t\tvar result fiber.Map\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\trequire.NoError(t, err)\n\t\terr = json.Unmarshal(body, &result)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, string(StateOpen), result[\"state\"])\n\t\trequire.Equal(t, false, result[\"healthy\"])\n\t})\n\n\tt.Run(\"Response Content Type\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(\"GET\", \"/health\", nil)\n\t\tresp, err := app.Test(req)\n\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"application/json; charset=utf-8\", resp.Header.Get(\"Content-Type\"))\n\t})\n\n\t// Clean up\n\tcb.Stop()\n}\n"
  },
  {
    "path": "v3/circuitbreaker/go.mod",
    "content": "module github.com/gofiber/contrib/v3/circuitbreaker\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/circuitbreaker/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/coraza/README.md",
    "content": "---\nid: coraza\n---\n\n# Coraza\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*coraza*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20Coraza/badge.svg)\n\n[Coraza](https://coraza.io/) WAF middleware for Fiber.\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get github.com/gofiber/fiber/v3\ngo get github.com/gofiber/contrib/v3/coraza\n```\n\n## Signature\n\n```go\ncoraza.New(config ...coraza.Config) fiber.Handler\ncoraza.NewEngine(config coraza.Config) (*coraza.Engine, error)\n```\n\n## Config\n\n| Property | Type | Description | Default |\n|:--|:--|:--|:--|\n| Next | `func(fiber.Ctx) bool` | Defines a function to skip this middleware when it returns true | `nil` |\n| BlockHandler | `func(fiber.Ctx, coraza.InterruptionDetails) error` | Custom handler for blocked requests | `nil` |\n| ErrorHandler | `func(fiber.Ctx, coraza.MiddlewareError) error` | Custom handler for middleware failures | `nil` |\n| DirectivesFile | `[]string` | Coraza directives files loaded in order | `nil` |\n| RootFS | `fs.FS` | Optional filesystem used to resolve `DirectivesFile` | `nil` |\n| BlockMessage | `string` | Message returned by the built-in block handler | `\"Request blocked by Web Application Firewall\"` |\n| LogLevel | `fiberlog.Level` | Middleware lifecycle log level | `fiberlog.LevelInfo` in `coraza.ConfigDefault` |\n| RequestBodyAccess | `bool` | Enables request body inspection | `true` in `coraza.ConfigDefault` |\n| MetricsCollector | `coraza.MetricsCollector` | Optional custom in-memory metrics collector | `nil` (falls back to the built-in collector) |\n\nIf you want the defaults, start from `coraza.ConfigDefault` and override the fields you need.\nFor zero-value-backed settings such as `RequestBodyAccess: false`, `LogLevel: fiberlog.LevelTrace`, or resetting `MetricsCollector` to the built-in default, use `ConfigDefault` or the helper methods `WithRequestBodyAccess`, `WithLogLevel`, and `WithMetricsCollector` so the choice remains explicit.\nBy default, the middleware starts without external rule files. Set `DirectivesFile` to load your Coraza or CRS ruleset.\nRequest body size follows the Fiber app `BodyLimit`.\nWildcard entries in `DirectivesFile` are expanded before Coraza initializes. If a wildcard matches no files, initialization fails with an error and the middleware does not start.\n\n## Usage\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"github.com/gofiber/contrib/v3/coraza\"\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n\tapp := fiber.New()\n\n\tcfg := coraza.ConfigDefault\n\tcfg.DirectivesFile = []string{\"./conf/coraza.conf\"}\n\n\tapp.Use(coraza.New(cfg))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tlog.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## Advanced usage with Engine\n\nUse `NewEngine` when you need explicit lifecycle control, reload support, or observability data.\n\n```go\nengineCfg := coraza.ConfigDefault\nengineCfg.DirectivesFile = []string{\"./conf/coraza.conf\"}\n\nengine, err := coraza.NewEngine(engineCfg)\nif err != nil {\n\tlog.Fatal(err)\n}\n\napp.Use(engine.Middleware(coraza.MiddlewareConfig{\n\tNext: func(c fiber.Ctx) bool {\n\t\treturn c.Path() == \"/healthz\"\n\t},\n\tBlockHandler: func(c fiber.Ctx, details coraza.InterruptionDetails) error {\n\t\treturn c.Status(details.StatusCode).JSON(fiber.Map{\n\t\t\t\"blocked\": true,\n\t\t\t\"rule_id\": details.RuleID,\n\t\t})\n\t},\n}))\n```\n\n## Engine observability\n\nThe middleware does not open operational routes for you, but `Engine` exposes data-oriented methods that can be used to build your own endpoints:\n\n- `engine.Reload()`\n- `engine.MetricsSnapshot()`\n- `engine.Snapshot()`\n- `engine.Report()`\n\n## Notes\n\n- Request headers and request bodies are inspected.\n- Request body size follows the Fiber app `BodyLimit`.\n- Response body inspection is not supported.\n- `coraza.New()` starts successfully without external rule files, but it does not load any rules until `DirectivesFile` is configured.\n- Invalid configuration causes `coraza.New(...)` to panic during startup, which allows applications to fail fast.\n\n## References\n\n- [Coraza Docs](https://coraza.io/)\n- [OWASP Core Rule Set](https://coraza.io/docs/tutorials/coreruleset)\n"
  },
  {
    "path": "v3/coraza/coraza.go",
    "content": "// Package coraza provides Coraza WAF middleware for Fiber.\npackage coraza\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/corazawaf/coraza/v3\"\n\t\"github.com/corazawaf/coraza/v3/experimental\"\n\t\"github.com/corazawaf/coraza/v3/types\"\n\t\"github.com/gofiber/fiber/v3\"\n\tfiberlog \"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nconst defaultBlockMessage = \"Request blocked by Web Application Firewall\"\n\n// Config defines the configuration for the Coraza middleware and Engine.\n//\n// For zero-value-backed fields such as RequestBodyAccess=false,\n// LogLevel=fiberlog.LevelTrace, or resetting MetricsCollector to the built-in\n// default, start from ConfigDefault or use the WithRequestBodyAccess,\n// WithLogLevel, and WithMetricsCollector helpers so the override remains\n// explicit.\ntype Config struct {\n\t// Next defines a function to skip this middleware when it returns true.\n\tNext func(fiber.Ctx) bool\n\t// BlockHandler customizes the response returned for interrupted requests.\n\tBlockHandler BlockHandler\n\t// ErrorHandler customizes the response returned for middleware failures.\n\tErrorHandler ErrorHandler\n\t// DirectivesFile lists Coraza directives files to load in order.\n\t// When empty, the engine starts without external rule files.\n\tDirectivesFile []string\n\t// RootFS is an optional filesystem used to resolve DirectivesFile entries.\n\tRootFS fs.FS\n\t// BlockMessage overrides the message used by the built-in block handler.\n\tBlockMessage string\n\t// LogLevel controls middleware lifecycle logging.\n\tLogLevel fiberlog.Level\n\t// RequestBodyAccess enables request body inspection in Coraza.\n\tRequestBodyAccess bool\n\t// MetricsCollector overrides the default in-memory metrics collector.\n\tMetricsCollector MetricsCollector\n\n\tlogLevelSet          bool\n\trequestBodyAccessSet bool\n\tmetricsCollectorSet  bool\n}\n\n// ConfigDefault provides the default Coraza configuration.\nvar ConfigDefault = Config{\n\tLogLevel:             fiberlog.LevelInfo,\n\tRequestBodyAccess:    true,\n\tlogLevelSet:          true,\n\trequestBodyAccessSet: true,\n}\n\n// MiddlewareConfig customizes how Engine middleware behaves for a specific mount.\ntype MiddlewareConfig struct {\n\t// Next bypasses WAF inspection when it returns true.\n\tNext func(fiber.Ctx) bool\n\t// BlockHandler customizes the response returned for interrupted requests.\n\tBlockHandler BlockHandler\n\t// ErrorHandler customizes the response returned for middleware failures.\n\tErrorHandler ErrorHandler\n}\n\n// MiddlewareError describes an operational failure that occurred while handling a request.\ntype MiddlewareError struct {\n\t// StatusCode is the HTTP status code suggested for the failure response.\n\tStatusCode int\n\t// Code is a stable application-level error code for the failure type.\n\tCode string\n\t// Message is the client-facing error message.\n\tMessage string\n\t// Err is the underlying error when one is available.\n\tErr error\n}\n\n// InterruptionDetails describes a Coraza interruption returned by request inspection.\ntype InterruptionDetails struct {\n\t// StatusCode is the HTTP status code associated with the interruption.\n\tStatusCode int\n\t// Action is the Coraza action, such as \"deny\".\n\tAction string\n\t// RuleID is the matched Coraza rule identifier when available.\n\tRuleID int\n\t// Data contains rule-specific interruption data when available.\n\tData string\n\t// Message is the message returned by the built-in block handler.\n\tMessage string\n}\n\n// BlockHandler handles requests that were interrupted by the WAF.\ntype BlockHandler func(fiber.Ctx, InterruptionDetails) error\n\n// ErrorHandler handles middleware errors that prevented request inspection.\ntype ErrorHandler func(fiber.Ctx, MiddlewareError) error\n\n// Engine owns a Coraza WAF instance and exposes Fiber middleware around it.\ntype Engine struct {\n\tmu sync.RWMutex\n\n\twaf             coraza.WAF\n\twafWithOptions  experimental.WAFWithOptions\n\tsupportsOptions bool\n\tinitErr         error\n\n\tactiveCfg      Config\n\tlastAttemptCfg Config\n\tblockMessage   string\n\tlogLevel       fiberlog.Level\n\tmetrics        MetricsCollector\n\treloadCount    uint64\n\tlastLoadedAt   time.Time\n\n\tinitSuccessCount   uint64\n\tinitFailureCount   uint64\n\treloadSuccessCount uint64\n\treloadFailureCount uint64\n}\n\n// New constructs Coraza Fiber middleware.\n//\n// It panics if the provided configuration cannot initialize a WAF instance.\nfunc New(config ...Config) fiber.Handler {\n\tcfg := ConfigDefault\n\tif len(config) > 0 {\n\t\tcfg = resolveConfig(config[0])\n\t}\n\n\tengine, err := NewEngine(cfg)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn engine.Middleware(MiddlewareConfig{\n\t\tNext:         cfg.Next,\n\t\tBlockHandler: cfg.BlockHandler,\n\t\tErrorHandler: cfg.ErrorHandler,\n\t})\n}\n\n// NewEngine creates and initializes an Engine with the provided configuration.\nfunc NewEngine(cfg Config) (*Engine, error) {\n\tengine := newEngine(nil)\n\tif err := engine.Init(cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn engine, nil\n}\n\n// Init replaces the Engine's WAF instance using the provided configuration.\n//\n// On failure, the last working WAF instance is kept in place and the failure is\n// recorded for observability.\nfunc (e *Engine) Init(cfg Config) error {\n\tresolvedCfg := resolveConfig(cfg)\n\tmetrics := resolveMetricsCollector(resolvedCfg.MetricsCollector)\n\n\tnewWAF, err := createWAFWithConfig(resolvedCfg)\n\tlogLevel := normalizeLogLevel(resolvedCfg.LogLevel)\n\n\te.mu.Lock()\n\tdefer e.mu.Unlock()\n\n\te.lastAttemptCfg = cloneConfig(resolvedCfg)\n\n\tif err != nil {\n\t\te.initErr = err\n\t\te.initFailureCount++\n\t\tlogWithLevel(logLevel, fiberlog.LevelError, \"Coraza initialization failed\", \"error\", err.Error())\n\t\treturn err\n\t}\n\n\te.waf = newWAF\n\te.initErr = nil\n\te.setWAFOptionsStateLocked(newWAF)\n\te.activeCfg = cloneConfig(resolvedCfg)\n\te.lastLoadedAt = time.Now()\n\te.initSuccessCount++\n\te.blockMessage = resolveBlockMessage(resolvedCfg.BlockMessage)\n\te.logLevel = logLevel\n\te.metrics = metrics\n\n\tlogWithLevel(logLevel, fiberlog.LevelInfo, \"Coraza initialized successfully\", \"supports_options\", e.supportsOptions)\n\treturn nil\n}\n\n// SetBlockMessage overrides the default message returned by the built-in block handler.\nfunc (e *Engine) SetBlockMessage(msg string) {\n\te.mu.Lock()\n\tdefer e.mu.Unlock()\n\te.blockMessage = resolveBlockMessage(msg)\n}\n\n// Metrics returns the Engine's metrics collector.\nfunc (e *Engine) Metrics() MetricsCollector {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\treturn e.metrics\n}\n\n// Middleware creates a Fiber middleware handler backed by the Engine's WAF instance.\nfunc (e *Engine) Middleware(config ...MiddlewareConfig) fiber.Handler {\n\tmwCfg := MiddlewareConfig{}\n\tif len(config) > 0 {\n\t\tmwCfg = config[0]\n\t}\n\n\treturn func(c fiber.Ctx) error {\n\t\tif mwCfg.Next != nil && mwCfg.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\tstartTime := time.Now()\n\t\tmetrics := e.Metrics()\n\t\tmetrics.RecordRequest()\n\n\t\tdefer func() {\n\t\t\tmetrics.RecordLatency(time.Since(startTime))\n\t\t}()\n\n\t\tcurrentWAF, currentSupportsOptions, currentWAFWithOptions, currentErr := e.snapshot()\n\t\tif currentWAF == nil {\n\t\t\tif currentErr != nil {\n\t\t\t\treturn e.handleError(c, mwCfg, MiddlewareError{\n\t\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\t\tCode:       \"waf_init_failed\",\n\t\t\t\t\tMessage:    \"WAF initialization failed\",\n\t\t\t\t\tErr:        currentErr,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn e.handleError(c, mwCfg, MiddlewareError{\n\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\tCode:       \"waf_not_initialized\",\n\t\t\t\tMessage:    \"WAF instance not initialized\",\n\t\t\t})\n\t\t}\n\n\t\tit, mwErr := e.inspectRequest(c, currentWAF, currentSupportsOptions, currentWAFWithOptions)\n\t\tif mwErr != nil {\n\t\t\treturn e.handleError(c, mwCfg, *mwErr)\n\t\t}\n\n\t\tif it != nil {\n\t\t\tmetrics.RecordBlock()\n\n\t\t\tdetails := InterruptionDetails{\n\t\t\t\tStatusCode: obtainStatusCodeFromInterruptionOrDefault(it, http.StatusForbidden),\n\t\t\t\tAction:     it.Action,\n\t\t\t\tRuleID:     it.RuleID,\n\t\t\t\tData:       it.Data,\n\t\t\t\tMessage:    e.blockMessageValue(),\n\t\t\t}\n\t\t\te.log(fiberlog.LevelWarn, \"Coraza request interrupted\",\n\t\t\t\t\"rule_id\", details.RuleID,\n\t\t\t\t\"action\", details.Action,\n\t\t\t\t\"status\", details.StatusCode)\n\n\t\t\tif mwCfg.BlockHandler != nil {\n\t\t\t\treturn mwCfg.BlockHandler(c, details)\n\t\t\t}\n\n\t\t\treturn defaultBlockHandler(c, details)\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n\nfunc (e *Engine) inspectRequest(\n\tc fiber.Ctx,\n\tcurrentWAF coraza.WAF,\n\tcurrentSupportsOptions bool,\n\tcurrentWAFWithOptions experimental.WAFWithOptions,\n) (_ *types.Interruption, mwErr *MiddlewareError) {\n\tvar tx types.Transaction\n\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\te.log(fiberlog.LevelError, \"Coraza panic recovered\",\n\t\t\t\t\"panic\", r,\n\t\t\t\t\"method\", c.Method(),\n\t\t\t\t\"path\", c.Path(),\n\t\t\t\t\"ip\", c.IP())\n\n\t\t\tmwErr = &MiddlewareError{\n\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\tCode:       \"waf_panic_recovered\",\n\t\t\t\tMessage:    \"WAF internal error\",\n\t\t\t\tErr:        fmt.Errorf(\"panic recovered: %v\", r),\n\t\t\t}\n\t\t}\n\n\t\tif tx != nil {\n\t\t\te.finishTransaction(c, tx, &mwErr)\n\t\t}\n\t}()\n\n\tstdReq, err := convertFiberToStdRequest(c)\n\tif err != nil {\n\t\treturn nil, &MiddlewareError{\n\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\tCode:       \"waf_request_convert_failed\",\n\t\t\tMessage:    \"Failed to convert request\",\n\t\t\tErr:        err,\n\t\t}\n\t}\n\n\tif currentSupportsOptions && currentWAFWithOptions != nil {\n\t\ttx = currentWAFWithOptions.NewTransactionWithOptions(experimental.Options{\n\t\t\tContext: stdReq.Context(),\n\t\t})\n\t} else {\n\t\ttx = currentWAF.NewTransaction()\n\t}\n\n\tif tx.IsRuleEngineOff() {\n\t\treturn nil, nil\n\t}\n\n\tit, err := processRequest(tx, stdReq, c.App().Config().BodyLimit)\n\tif err != nil {\n\t\treturn nil, &MiddlewareError{\n\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\tCode:       \"waf_request_processing_failed\",\n\t\t\tMessage:    \"WAF request processing failed\",\n\t\t\tErr:        err,\n\t\t}\n\t}\n\n\treturn it, nil\n}\n\nfunc (e *Engine) finishTransaction(c fiber.Ctx, tx types.Transaction, mwErr **MiddlewareError) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\te.log(fiberlog.LevelError, \"Coraza cleanup panic recovered\",\n\t\t\t\t\"panic\", r,\n\t\t\t\t\"method\", c.Method(),\n\t\t\t\t\"path\", c.Path(),\n\t\t\t\t\"ip\", c.IP())\n\n\t\t\tif *mwErr == nil {\n\t\t\t\t*mwErr = &MiddlewareError{\n\t\t\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t\t\t\tCode:       \"waf_cleanup_panic_recovered\",\n\t\t\t\t\tMessage:    \"WAF internal error\",\n\t\t\t\t\tErr:        fmt.Errorf(\"cleanup panic recovered: %v\", r),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\ttx.ProcessLogging()\n\tif err := tx.Close(); err != nil {\n\t\te.log(fiberlog.LevelDebug, \"Coraza transaction close failed\", \"error\", err.Error())\n\t}\n}\n\n// Reload rebuilds the current WAF instance using the active configuration.\nfunc (e *Engine) Reload() error {\n\te.mu.RLock()\n\tcfg := cloneConfig(e.activeCfg)\n\te.mu.RUnlock()\n\n\tlogLevel := normalizeLogLevel(cfg.LogLevel)\n\tlogWithLevel(logLevel, fiberlog.LevelInfo, \"Coraza starting manual reload\")\n\n\tnewWAF, err := createWAFWithConfig(cfg)\n\tif err != nil {\n\t\te.mu.Lock()\n\t\te.reloadFailureCount++\n\t\te.mu.Unlock()\n\t\tlogWithLevel(logLevel, fiberlog.LevelError, \"Coraza reload failed\", \"error\", err.Error())\n\t\treturn fmt.Errorf(\"failed to reload WAF: %w\", err)\n\t}\n\n\te.mu.Lock()\n\te.waf = newWAF\n\te.initErr = nil\n\te.setWAFOptionsStateLocked(newWAF)\n\te.reloadCount++\n\te.reloadSuccessCount++\n\te.lastLoadedAt = time.Now()\n\treloadCount := e.reloadCount\n\te.logLevel = logLevel\n\te.mu.Unlock()\n\n\tlogWithLevel(logLevel, fiberlog.LevelInfo, \"Coraza reload completed successfully\", \"reload_count\", reloadCount)\n\treturn nil\n}\n\nfunc (e *Engine) snapshot() (coraza.WAF, bool, experimental.WAFWithOptions, error) {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\n\treturn e.waf, e.supportsOptions, e.wafWithOptions, e.initErr\n}\n\nfunc (e *Engine) setWAFOptionsStateLocked(waf coraza.WAF) {\n\tif wafWithOptions, ok := waf.(experimental.WAFWithOptions); ok {\n\t\te.wafWithOptions = wafWithOptions\n\t\te.supportsOptions = true\n\t\treturn\n\t}\n\n\te.wafWithOptions = nil\n\te.supportsOptions = false\n}\n\nfunc (e *Engine) blockMessageValue() string {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\treturn e.blockMessage\n}\n\nfunc (e *Engine) handleError(c fiber.Ctx, cfg MiddlewareConfig, mwErr MiddlewareError) error {\n\tif cfg.ErrorHandler != nil {\n\t\treturn cfg.ErrorHandler(c, mwErr)\n\t}\n\n\treturn defaultErrorHandler(c, mwErr)\n}\n\nfunc newEngine(collector MetricsCollector) *Engine {\n\treturn &Engine{\n\t\tblockMessage: defaultBlockMessage,\n\t\tlogLevel:     fiberlog.LevelInfo,\n\t\tmetrics:      resolveMetricsCollector(collector),\n\t}\n}\n\nfunc isNilMetricsCollector(collector MetricsCollector) bool {\n\tif collector == nil {\n\t\treturn true\n\t}\n\n\tvalue := reflect.ValueOf(collector)\n\tswitch value.Kind() {\n\tcase reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:\n\t\treturn value.IsNil()\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc resolveMetricsCollector(collector MetricsCollector) MetricsCollector {\n\tif isNilMetricsCollector(collector) {\n\t\treturn NewDefaultMetricsCollector()\n\t}\n\n\treturn collector\n}\n\nfunc (e *Engine) observabilitySnapshot() EngineSnapshot {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\n\tvar lastInitError string\n\tif e.initErr != nil {\n\t\tlastInitError = e.initErr.Error()\n\t}\n\n\tconfigFiles := append([]string(nil), e.activeCfg.DirectivesFile...)\n\tlastAttemptConfigFiles := append([]string(nil), e.lastAttemptCfg.DirectivesFile...)\n\n\treturn EngineSnapshot{\n\t\tInitialized:            e.waf != nil,\n\t\tSupportsOptions:        e.supportsOptions,\n\t\tConfigFiles:            configFiles,\n\t\tLastAttemptConfigFiles: lastAttemptConfigFiles,\n\t\tLastInitError:          lastInitError,\n\t\tLastLoadedAt:           e.lastLoadedAt,\n\t\tInitSuccessTotal:       e.initSuccessCount,\n\t\tInitFailureTotal:       e.initFailureCount,\n\t\tReloadSuccessTotal:     e.reloadSuccessCount,\n\t\tReloadFailureTotal:     e.reloadFailureCount,\n\t\tReloadCount:            e.reloadCount,\n\t}\n}\n\nfunc defaultBlockHandler(c fiber.Ctx, details InterruptionDetails) error {\n\tc.Set(\"X-WAF-Blocked\", \"true\")\n\treturn fiber.NewError(details.StatusCode, details.Message)\n}\n\nfunc defaultErrorHandler(_ fiber.Ctx, mwErr MiddlewareError) error {\n\treturn fiber.NewError(mwErr.StatusCode, mwErr.Message)\n}\n\nfunc processRequest(tx types.Transaction, req *http.Request, bodyLimit int) (*types.Interruption, error) {\n\tclient, cport := splitRemoteAddr(req.RemoteAddr)\n\n\ttx.ProcessConnection(client, cport, \"\", 0)\n\ttx.ProcessURI(req.URL.String(), req.Method, req.Proto)\n\n\tfor k, values := range req.Header {\n\t\tfor _, v := range values {\n\t\t\ttx.AddRequestHeader(k, v)\n\t\t}\n\t}\n\n\tif req.Host != \"\" {\n\t\ttx.AddRequestHeader(\"Host\", req.Host)\n\t\ttx.SetServerName(req.Host)\n\t}\n\n\tfor _, te := range req.TransferEncoding {\n\t\ttx.AddRequestHeader(\"Transfer-Encoding\", te)\n\t}\n\n\tif in := tx.ProcessRequestHeaders(); in != nil {\n\t\treturn in, nil\n\t}\n\n\tif tx.IsRequestBodyAccessible() && req.Body != nil && req.Body != http.NoBody {\n\t\tbodyReader := io.Reader(req.Body)\n\t\tif bodyLimit > 0 {\n\t\t\tbodyReader = io.LimitReader(req.Body, int64(bodyLimit))\n\t\t}\n\n\t\tit, _, err := tx.ReadRequestBodyFrom(bodyReader)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif it != nil {\n\t\t\treturn it, nil\n\t\t}\n\t}\n\n\treturn tx.ProcessRequestBody()\n}\n\nfunc obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultStatusCode int) int {\n\tif it.Action == \"deny\" {\n\t\tif it.Status != 0 {\n\t\t\treturn it.Status\n\t\t}\n\t\treturn http.StatusForbidden\n\t}\n\n\treturn defaultStatusCode\n}\n\nfunc convertFiberToStdRequest(c fiber.Ctx) (*http.Request, error) {\n\treq, err := adaptor.ConvertRequest(c, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.RemoteAddr = net.JoinHostPort(c.IP(), c.Port())\n\tif req.Host == \"\" {\n\t\treq.Host = c.Hostname()\n\t}\n\n\treturn req, nil\n}\n\nfunc createWAFWithConfig(cfg Config) (coraza.WAF, error) {\n\tvar directivesFiles []string\n\tlogLevel := normalizeLogLevel(cfg.LogLevel)\n\tfor _, path := range cfg.DirectivesFile {\n\t\texpandedPaths, err := resolveDirectivesFiles(cfg.RootFS, path, logLevel)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdirectivesFiles = append(directivesFiles, expandedPaths...)\n\t}\n\n\twafConfig := coraza.NewWAFConfig()\n\n\tif cfg.RequestBodyAccess {\n\t\twafConfig = wafConfig.WithRequestBodyAccess()\n\t}\n\tif cfg.RootFS != nil {\n\t\twafConfig = wafConfig.WithRootFS(cfg.RootFS)\n\t}\n\n\tfor _, path := range directivesFiles {\n\t\twafConfig = wafConfig.WithDirectivesFromFile(path)\n\t}\n\n\treturn coraza.NewWAF(wafConfig)\n}\n\nfunc resolveDirectivesFiles(root fs.FS, path string, logLevel fiberlog.Level) ([]string, error) {\n\tif strings.ContainsAny(path, \"*?[\") {\n\t\tlogWithLevel(logLevel, fiberlog.LevelWarn,\n\t\t\t\"Coraza directives path uses glob matching and is expanded before initialization\",\n\t\t\t\"path\", path,\n\t\t\t\"note\", \"all matching directives files will be loaded in sorted order\",\n\t\t)\n\n\t\tvar (\n\t\t\tmatches []string\n\t\t\terr     error\n\t\t)\n\t\tif root != nil {\n\t\t\tmatches, err = fs.Glob(root, path)\n\t\t} else {\n\t\t\tmatches, err = filepath.Glob(path)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid Coraza directives glob %q: %w\", path, err)\n\t\t}\n\t\tif len(matches) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"coraza directives glob %q matched no files\", path)\n\t\t}\n\n\t\treturn matches, nil\n\t}\n\n\tif root != nil {\n\t\tif _, err := fs.Stat(root, path); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"coraza directives file %q not found in RootFS: %w\", path, err)\n\t\t}\n\t\treturn []string{path}, nil\n\t}\n\n\tif _, err := os.Stat(path); err != nil {\n\t\treturn nil, fmt.Errorf(\"coraza directives file %q not found: %w\", path, err)\n\t}\n\n\treturn []string{path}, nil\n}\n\nfunc splitRemoteAddr(remoteAddr string) (string, int) {\n\thost, port, err := net.SplitHostPort(remoteAddr)\n\tif err != nil {\n\t\treturn remoteAddr, 0\n\t}\n\n\tportNum, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn host, 0\n\t}\n\n\treturn host, portNum\n}\n\nfunc cloneConfig(cfg Config) Config {\n\tclone := cfg\n\tclone.DirectivesFile = append([]string(nil), cfg.DirectivesFile...)\n\treturn clone\n}\n\nfunc resolveConfig(cfg Config) Config {\n\tresolved := ConfigDefault\n\n\tif cfg.Next != nil {\n\t\tresolved.Next = cfg.Next\n\t}\n\tif cfg.BlockHandler != nil {\n\t\tresolved.BlockHandler = cfg.BlockHandler\n\t}\n\tif cfg.ErrorHandler != nil {\n\t\tresolved.ErrorHandler = cfg.ErrorHandler\n\t}\n\tif cfg.DirectivesFile != nil {\n\t\tresolved.DirectivesFile = append([]string(nil), cfg.DirectivesFile...)\n\t}\n\tif cfg.RootFS != nil {\n\t\tresolved.RootFS = cfg.RootFS\n\t}\n\tif cfg.BlockMessage != \"\" {\n\t\tresolved.BlockMessage = cfg.BlockMessage\n\t}\n\tif cfg.logLevelSet || cfg.LogLevel != 0 {\n\t\tresolved.LogLevel = normalizeLogLevel(cfg.LogLevel)\n\t}\n\tif cfg.requestBodyAccessSet || cfg.RequestBodyAccess {\n\t\tresolved.RequestBodyAccess = cfg.RequestBodyAccess\n\t}\n\tif cfg.metricsCollectorSet || !isNilMetricsCollector(cfg.MetricsCollector) {\n\t\tresolved.MetricsCollector = cfg.MetricsCollector\n\t}\n\n\tresolved.logLevelSet = true\n\tresolved.requestBodyAccessSet = true\n\tresolved.metricsCollectorSet = cfg.metricsCollectorSet || !isNilMetricsCollector(cfg.MetricsCollector)\n\n\treturn resolved\n}\n\n// WithLogLevel returns a copy of cfg with an explicit lifecycle log level.\nfunc (cfg Config) WithLogLevel(level fiberlog.Level) Config {\n\tcfg.LogLevel = level\n\tcfg.logLevelSet = true\n\treturn cfg\n}\n\n// WithRequestBodyAccess returns a copy of cfg with explicit request body inspection behavior.\nfunc (cfg Config) WithRequestBodyAccess(enabled bool) Config {\n\tcfg.RequestBodyAccess = enabled\n\tcfg.requestBodyAccessSet = true\n\treturn cfg\n}\n\n// WithMetricsCollector returns a copy of cfg with an explicit metrics collector choice.\nfunc (cfg Config) WithMetricsCollector(collector MetricsCollector) Config {\n\tcfg.MetricsCollector = collector\n\tcfg.metricsCollectorSet = true\n\treturn cfg\n}\n\nfunc resolveBlockMessage(msg string) string {\n\tif msg == \"\" {\n\t\treturn defaultBlockMessage\n\t}\n\n\treturn msg\n}\n\nfunc normalizeLogLevel(level fiberlog.Level) fiberlog.Level {\n\tswitch level {\n\tcase fiberlog.LevelTrace, fiberlog.LevelDebug, fiberlog.LevelInfo, fiberlog.LevelWarn, fiberlog.LevelError:\n\t\treturn level\n\tdefault:\n\t\treturn fiberlog.LevelInfo\n\t}\n}\n\nfunc logWithLevel(configLevel, targetLevel fiberlog.Level, msg string, keysAndValues ...any) {\n\tif normalizeLogLevel(configLevel) > normalizeLogLevel(targetLevel) {\n\t\treturn\n\t}\n\n\tswitch targetLevel {\n\tcase fiberlog.LevelTrace:\n\t\tfiberlog.Tracew(msg, keysAndValues...)\n\tcase fiberlog.LevelDebug:\n\t\tfiberlog.Debugw(msg, keysAndValues...)\n\tcase fiberlog.LevelWarn:\n\t\tfiberlog.Warnw(msg, keysAndValues...)\n\tcase fiberlog.LevelError:\n\t\tfiberlog.Errorw(msg, keysAndValues...)\n\tdefault:\n\t\tfiberlog.Infow(msg, keysAndValues...)\n\t}\n}\n\nfunc (e *Engine) currentLogLevel() fiberlog.Level {\n\te.mu.RLock()\n\tdefer e.mu.RUnlock()\n\treturn e.logLevel\n}\n\nfunc (e *Engine) log(targetLevel fiberlog.Level, msg string, keysAndValues ...any) {\n\tlogWithLevel(e.currentLogLevel(), targetLevel, msg, keysAndValues...)\n}\n"
  },
  {
    "path": "v3/coraza/coraza_test.go",
    "content": "package coraza\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/corazawaf/coraza/v3/debuglog\"\n\t\"github.com/corazawaf/coraza/v3/types\"\n\t\"github.com/gofiber/fiber/v3\"\n\tfiberlog \"github.com/gofiber/fiber/v3/log\"\n)\n\nconst testRules = `SecRuleEngine On\nSecRequestBodyAccess On\nSecRule ARGS:attack \"@streq 1\" \"id:1001,phase:2,deny,status:403,msg:'attack detected'\"`\n\nfunc TestNewPanicsOnInvalidConfig(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatal(\"expected New to panic when config is invalid\")\n\t\t}\n\t}()\n\n\t_ = New(Config{DirectivesFile: []string{\"missing.conf\"}})\n}\n\nfunc TestNewWithoutConfigReturnsMiddleware(t *testing.T) {\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\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/\", nil))\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read response body: %v\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected status 200, got %d\", resp.StatusCode)\n\t}\n\tif string(body) != \"ok\" {\n\t\tt.Fatalf(\"expected body ok, got %q\", string(body))\n\t}\n}\n\nfunc TestNewEngineWithLocalFile(t *testing.T) {\n\tpath := writeRuleFile(t, t.TempDir(), \"local.conf\", testRules)\n\tengine, err := NewEngine(Config{\n\t\tLogLevel:          fiberlog.LevelInfo,\n\t\tDirectivesFile:    []string{path},\n\t\tBlockMessage:      \"blocked from config\",\n\t\tRequestBodyAccess: true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"expected successful initialization, got error: %v\", err)\n\t}\n\n\tif engine.initErr != nil {\n\t\tt.Fatalf(\"expected successful initialization, got error: %v\", engine.initErr)\n\t}\n\tif engine.waf == nil {\n\t\tt.Fatal(\"expected engine WAF to be initialized\")\n\t}\n\tif engine.blockMessage != \"blocked from config\" {\n\t\tt.Fatalf(\"expected block message to be initialized from config, got %q\", engine.blockMessage)\n\t}\n}\n\nfunc TestSetBlockMessageEmptyResetsDefault(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tengine.SetBlockMessage(\"custom block\")\n\tengine.SetBlockMessage(\"\")\n\n\tif got := engine.blockMessageValue(); got != defaultBlockMessage {\n\t\tt.Fatalf(\"expected empty block message to restore default, got %q\", got)\n\t}\n}\n\nfunc TestNewEngineWithRootFS(t *testing.T) {\n\ttempDir := t.TempDir()\n\twriteRuleFile(t, tempDir, \"rootfs.conf\", testRules)\n\n\tengine, err := NewEngine(Config{\n\t\tDirectivesFile:    []string{\"rootfs.conf\"},\n\t\tRootFS:            os.DirFS(tempDir),\n\t\tRequestBodyAccess: true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"expected RootFS initialization to succeed, got error: %v\", err)\n\t}\n\n\tif engine.initErr != nil {\n\t\tt.Fatalf(\"expected RootFS initialization to succeed, got error: %v\", engine.initErr)\n\t}\n\tif engine.waf == nil {\n\t\tt.Fatal(\"expected engine WAF to be initialized from RootFS\")\n\t}\n}\n\nfunc TestNewEngineMissingFile(t *testing.T) {\n\t_, err := NewEngine(Config{\n\t\tDirectivesFile: []string{\"missing.conf\"},\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"expected initialization to fail for missing directives file\")\n\t}\n}\n\nfunc TestNewReturnsMiddleware(t *testing.T) {\n\tpath := writeRuleFile(t, t.TempDir(), \"test.conf\", testRules)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tDirectivesFile:    []string{path},\n\t\tRequestBodyAccess: true,\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?attack=1\", nil))\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusForbidden {\n\t\tt.Fatalf(\"expected status 403, got %d\", resp.StatusCode)\n\t}\n}\n\nfunc TestNewAppliesConfigDefaults(t *testing.T) {\n\tbodyRules := `SecRuleEngine On\nSecRequestBodyAccess On\nSecRule REQUEST_BODY \"@contains attack\" \"id:1002,phase:2,deny,status:403,msg:'body attack detected'\"`\n\n\tpath := writeRuleFile(t, t.TempDir(), \"body.conf\", bodyRules)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tDirectivesFile: []string{path},\n\t}))\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"payload=attack\"))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp := performRequest(t, app, req)\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusForbidden {\n\t\tt.Fatalf(\"expected status 403 when New applies default request body access, got %d\", resp.StatusCode)\n\t}\n}\n\nfunc TestResolveConfigHonorsExplicitZeroValueOverrides(t *testing.T) {\n\tresolved := resolveConfig(Config{}.WithRequestBodyAccess(false).WithLogLevel(fiberlog.LevelTrace))\n\n\tif resolved.RequestBodyAccess {\n\t\tt.Fatal(\"expected explicit request body access override to remain false\")\n\t}\n\tif resolved.LogLevel != fiberlog.LevelTrace {\n\t\tt.Fatalf(\"expected explicit trace log level override, got %v\", resolved.LogLevel)\n\t}\n}\n\nfunc TestEngineMiddlewareAllowsCleanRequest(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\treq := httptest.NewRequest(http.MethodGet, \"/?name=safe\", nil)\n\n\tresp := performRequest(t, app, req)\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read response body: %v\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected status 200, got %d\", resp.StatusCode)\n\t}\n\tif string(body) != \"ok\" {\n\t\tt.Fatalf(\"expected body ok, got %q\", string(body))\n\t}\n\n\tmetrics := engine.MetricsSnapshot()\n\tif metrics.TotalRequests != 1 || metrics.BlockedRequests != 0 {\n\t\tt.Fatalf(\"unexpected metrics after clean request: %+v\", metrics)\n\t}\n}\n\nfunc TestEngineMiddlewareBlocksMaliciousRequest(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\treq := httptest.NewRequest(http.MethodGet, \"/?attack=1\", nil)\n\n\tresp := performRequest(t, app, req)\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read response body: %v\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusForbidden {\n\t\tt.Fatalf(\"expected status 403, got %d\", resp.StatusCode)\n\t}\n\tif resp.Header.Get(\"X-WAF-Blocked\") != \"true\" {\n\t\tt.Fatalf(\"expected X-WAF-Blocked header to be true, got %q\", resp.Header.Get(\"X-WAF-Blocked\"))\n\t}\n\tif !strings.Contains(string(body), defaultBlockMessage) {\n\t\tt.Fatalf(\"expected block message in response body, got %q\", string(body))\n\t}\n\n\tmetrics := engine.MetricsSnapshot()\n\tif metrics.TotalRequests != 1 || metrics.BlockedRequests != 1 {\n\t\tt.Fatalf(\"unexpected metrics after blocked request: %+v\", metrics)\n\t}\n}\n\nfunc TestEngineMiddlewareBlocksMaliciousRequestBody(t *testing.T) {\n\tbodyRules := `SecRuleEngine On\nSecRequestBodyAccess On\nSecRule REQUEST_BODY \"@contains attack\" \"id:1002,phase:2,deny,status:403,msg:'body attack detected'\"`\n\n\tengine, err := newTestEngineWithRules(t, bodyRules)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"payload=attack\"))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp := performRequest(t, app, req)\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusForbidden {\n\t\tt.Fatalf(\"expected status 403 for malicious body, got %d\", resp.StatusCode)\n\t}\n\n\tmetrics := engine.MetricsSnapshot()\n\tif metrics.TotalRequests != 1 || metrics.BlockedRequests != 1 {\n\t\tt.Fatalf(\"unexpected metrics after blocked body request: %+v\", metrics)\n\t}\n}\n\nfunc TestEngineMiddlewareRespectsFiberBodyLimit(t *testing.T) {\n\tbodyRules := `SecRuleEngine On\nSecRequestBodyAccess On\nSecRule REQUEST_BODY \"@contains attack\" \"id:1002,phase:2,deny,status:403,msg:'body attack detected'\"`\n\n\tengine, err := newTestEngineWithRules(t, bodyRules)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := fiber.New(fiber.Config{\n\t\tBodyLimit: 8,\n\t})\n\tapp.Use(engine.Middleware())\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\n\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(\"payload=attack\"))\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\t_, err = app.Test(req)\n\tif err == nil {\n\t\tt.Fatal(\"expected Fiber body limit error, got nil\")\n\t}\n\tif err.Error() != \"body size exceeds the given limit\" {\n\t\tt.Fatalf(\"expected Fiber body limit error, got %v\", err)\n\t}\n}\n\nfunc TestNewEngineProvidesInstanceIsolation(t *testing.T) {\n\tfirst, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create first engine: %v\", err)\n\t}\n\tsecond, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create second engine: %v\", err)\n\t}\n\n\tfirst.SetBlockMessage(\"blocked by first\")\n\tsecond.SetBlockMessage(\"blocked by second\")\n\n\tfirstApp := newInstanceApp(first, MiddlewareConfig{})\n\tsecondApp := newInstanceApp(second, MiddlewareConfig{})\n\n\tfirstResp := performRequest(t, firstApp, httptest.NewRequest(http.MethodGet, \"/?attack=1\", nil))\n\tdefer firstResp.Body.Close()\n\tfirstBody, err := io.ReadAll(firstResp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read first response body: %v\", err)\n\t}\n\n\tsecondResp := performRequest(t, secondApp, httptest.NewRequest(http.MethodGet, \"/?attack=1\", nil))\n\tdefer secondResp.Body.Close()\n\tsecondBody, err := io.ReadAll(secondResp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read second response body: %v\", err)\n\t}\n\n\tif !strings.Contains(string(firstBody), \"blocked by first\") {\n\t\tt.Fatalf(\"expected first engine response to contain its block message, got %q\", string(firstBody))\n\t}\n\tif !strings.Contains(string(secondBody), \"blocked by second\") {\n\t\tt.Fatalf(\"expected second engine response to contain its block message, got %q\", string(secondBody))\n\t}\n}\n\nfunc TestMiddlewareConfigNextBypassesInspection(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Query(\"attack\") == \"1\"\n\t\t},\n\t})\n\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?attack=1\", nil))\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected skipped request to pass through, got %d\", resp.StatusCode)\n\t}\n\n\tmetrics := engine.MetricsSnapshot()\n\tif metrics.TotalRequests != 0 || metrics.BlockedRequests != 0 {\n\t\tt.Fatalf(\"expected skipped request not to affect metrics, got %+v\", metrics)\n\t}\n}\n\nfunc TestMiddlewareConfigCustomBlockHandler(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{\n\t\tBlockHandler: func(c fiber.Ctx, details InterruptionDetails) error {\n\t\t\tc.Set(\"X-Custom-Block\", \"true\")\n\t\t\treturn c.Status(http.StatusTeapot).JSON(fiber.Map{\n\t\t\t\t\"rule_id\": details.RuleID,\n\t\t\t\t\"status\":  details.StatusCode,\n\t\t\t})\n\t\t},\n\t})\n\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?attack=1\", nil))\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read custom block response body: %v\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusTeapot {\n\t\tt.Fatalf(\"expected custom block status 418, got %d\", resp.StatusCode)\n\t}\n\tif resp.Header.Get(\"X-Custom-Block\") != \"true\" {\n\t\tt.Fatalf(\"expected custom block header, got %q\", resp.Header.Get(\"X-Custom-Block\"))\n\t}\n\tif !strings.Contains(string(body), `\"rule_id\":1001`) {\n\t\tt.Fatalf(\"expected custom block body to include rule id, got %q\", string(body))\n\t}\n}\n\nfunc TestMiddlewareConfigCustomErrorHandler(t *testing.T) {\n\tengine := newEngine(NewDefaultMetricsCollector())\n\tapp := newInstanceApp(engine, MiddlewareConfig{\n\t\tErrorHandler: func(c fiber.Ctx, mwErr MiddlewareError) error {\n\t\t\treturn c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{\n\t\t\t\t\"error_code\": mwErr.Code,\n\t\t\t\t\"message\":    mwErr.Message,\n\t\t\t})\n\t\t},\n\t})\n\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/\", nil))\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read custom error response body: %v\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusServiceUnavailable {\n\t\tt.Fatalf(\"expected custom error status 503, got %d\", resp.StatusCode)\n\t}\n\tif !strings.Contains(string(body), `\"error_code\":\"waf_not_initialized\"`) {\n\t\tt.Fatalf(\"expected custom error code in body, got %q\", string(body))\n\t}\n}\n\nfunc TestEngineReportIncludesLifecycleSnapshot(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\treport := engine.Report()\n\tif !report.Engine.Initialized {\n\t\tt.Fatal(\"expected report to include initialized engine state\")\n\t}\n\tif report.Engine.InitSuccessTotal != 1 {\n\t\tt.Fatalf(\"expected init success count to be 1, got %d\", report.Engine.InitSuccessTotal)\n\t}\n\tif len(report.Engine.ConfigFiles) != 1 {\n\t\tt.Fatalf(\"expected one config file in report, got %+v\", report.Engine.ConfigFiles)\n\t}\n}\n\nfunc TestMetricsSnapshotHandlesNilCollectorSnapshot(t *testing.T) {\n\tengine := newEngine(nilSnapshotCollector{})\n\n\tsnapshot := engine.MetricsSnapshot()\n\n\tif snapshot.TotalRequests != 0 || snapshot.BlockedRequests != 0 || snapshot.AvgLatencyMs != 0 || snapshot.BlockRate != 0 {\n\t\tt.Fatalf(\"expected zero-value metrics snapshot, got %+v\", snapshot)\n\t}\n\tif snapshot.Timestamp.IsZero() {\n\t\tt.Fatal(\"expected metrics snapshot timestamp to be populated\")\n\t}\n}\n\nfunc TestNewEngineFallsBackToDefaultCollectorForTypedNilMetricsCollector(t *testing.T) {\n\tvar collector MetricsCollector = (*nilPtrSnapshotCollector)(nil)\n\n\tengine := newEngine(collector)\n\n\tif engine.Metrics() == nil {\n\t\tt.Fatal(\"expected typed-nil metrics collector to fall back to the default collector\")\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/\", nil))\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusInternalServerError {\n\t\tt.Fatalf(\"expected status 500 with uninitialized WAF, got %d\", resp.StatusCode)\n\t}\n\n\tsnapshot := engine.MetricsSnapshot()\n\tif snapshot.TotalRequests != 1 {\n\t\tt.Fatalf(\"expected fallback collector to record one request, got %+v\", snapshot)\n\t}\n}\n\nfunc TestEngineInitFailureKeepsLastWorkingWAF(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\n\tallowedBefore := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?name=safe\", nil))\n\tdefer allowedBefore.Body.Close()\n\tif allowedBefore.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected status 200 before failed reinit, got %d\", allowedBefore.StatusCode)\n\t}\n\n\terr = engine.Init(Config{DirectivesFile: []string{filepath.Join(t.TempDir(), \"missing.conf\")}})\n\tif err == nil {\n\t\tt.Fatal(\"expected reinitialization with missing config to fail\")\n\t}\n\n\tallowedAfter := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?name=safe\", nil))\n\tdefer allowedAfter.Body.Close()\n\tbody, err := io.ReadAll(allowedAfter.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read response body after failed reinit: %v\", err)\n\t}\n\n\tif allowedAfter.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected last working WAF to continue serving after failed reinit, got %d with body %q\", allowedAfter.StatusCode, string(body))\n\t}\n\n\tsnapshot := engine.Snapshot()\n\tif snapshot.LastInitError == \"\" {\n\t\tt.Fatal(\"expected engine snapshot to retain the last initialization error for observability\")\n\t}\n\tif len(snapshot.ConfigFiles) != 1 || !strings.HasSuffix(snapshot.ConfigFiles[0], \"test.conf\") {\n\t\tt.Fatalf(\"expected active config to remain unchanged, got %+v\", snapshot.ConfigFiles)\n\t}\n\tif len(snapshot.LastAttemptConfigFiles) != 1 || !strings.HasSuffix(snapshot.LastAttemptConfigFiles[0], \"missing.conf\") {\n\t\tt.Fatalf(\"expected last attempted config to be reported, got %+v\", snapshot.LastAttemptConfigFiles)\n\t}\n}\n\nfunc TestMiddlewareFailsClosedWhenWAFPanicOccurs(t *testing.T) {\n\tengine := newEngine(NewDefaultMetricsCollector())\n\tengine.waf = fakePanicWAF{}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?name=safe\", nil))\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read panic recovery response body: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusInternalServerError {\n\t\tt.Fatalf(\"expected status 500 when WAF panics, got %d\", resp.StatusCode)\n\t}\n\tif !strings.Contains(string(body), \"WAF internal error\") {\n\t\tt.Fatalf(\"expected WAF internal error response, got %q\", string(body))\n\t}\n\n\tmetrics := engine.MetricsSnapshot()\n\tif metrics.TotalRequests != 1 || metrics.BlockedRequests != 0 {\n\t\tt.Fatalf(\"unexpected metrics after panic recovery: %+v\", metrics)\n\t}\n}\n\nfunc TestEngineSnapshotTracksLifecycleCounters(t *testing.T) {\n\tengine, err := newTestEngine(t)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine: %v\", err)\n\t}\n\n\tif err := engine.Reload(); err != nil {\n\t\tt.Fatalf(\"expected reload to succeed, got %v\", err)\n\t}\n\n\tsnapshot := engine.Snapshot()\n\tif snapshot.ReloadSuccessTotal != 1 {\n\t\tt.Fatalf(\"expected ReloadSuccessTotal=1, got %#v\", snapshot.ReloadSuccessTotal)\n\t}\n\tif snapshot.InitSuccessTotal != 1 {\n\t\tt.Fatalf(\"expected InitSuccessTotal=1, got %#v\", snapshot.InitSuccessTotal)\n\t}\n\tif snapshot.ReloadCount != 1 {\n\t\tt.Fatalf(\"expected ReloadCount=1, got %#v\", snapshot.ReloadCount)\n\t}\n}\n\nfunc TestEngineInitReplacesMetricsCollectorWhenProvided(t *testing.T) {\n\tinitialCollector := &countingCollector{}\n\tengine := newEngine(initialCollector)\n\n\tpath := writeRuleFile(t, t.TempDir(), \"collector.conf\", testRules)\n\treplacementCollector := &countingCollector{}\n\n\tif err := engine.Init(Config{\n\t\tDirectivesFile:    []string{path},\n\t\tRequestBodyAccess: true,\n\t\tMetricsCollector:  replacementCollector,\n\t}); err != nil {\n\t\tt.Fatalf(\"expected init to succeed, got %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?name=safe\", nil))\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected status 200 after init, got %d\", resp.StatusCode)\n\t}\n\tif initialCollector.requests != 0 {\n\t\tt.Fatalf(\"expected initial collector to stop receiving updates, got %d requests\", initialCollector.requests)\n\t}\n\tif replacementCollector.requests != 1 {\n\t\tt.Fatalf(\"expected replacement collector to record one request, got %d\", replacementCollector.requests)\n\t}\n}\n\nfunc TestEngineInitResetsMetricsCollectorToDefaultWhenOmitted(t *testing.T) {\n\tinitialCollector := &countingCollector{}\n\tengine, err := NewEngine(Config{\n\t\tDirectivesFile: []string{writeRuleFile(t, t.TempDir(), \"collector.conf\", testRules)},\n\t}.WithMetricsCollector(initialCollector))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create engine with custom collector: %v\", err)\n\t}\n\n\treloadPath := writeRuleFile(t, t.TempDir(), \"collector-reload.conf\", testRules)\n\tif err := engine.Init(Config{\n\t\tDirectivesFile: []string{reloadPath},\n\t}); err != nil {\n\t\tt.Fatalf(\"expected reinit without collector to succeed, got %v\", err)\n\t}\n\n\tapp := newInstanceApp(engine, MiddlewareConfig{})\n\tresp := performRequest(t, app, httptest.NewRequest(http.MethodGet, \"/?name=safe\", nil))\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected status 200 after collector reset, got %d\", resp.StatusCode)\n\t}\n\tif initialCollector.requests != 0 {\n\t\tt.Fatalf(\"expected original custom collector to stop receiving updates, got %d requests\", initialCollector.requests)\n\t}\n\n\tsnapshot := engine.MetricsSnapshot()\n\tif snapshot.TotalRequests != 1 {\n\t\tt.Fatalf(\"expected default collector to record one request after reset, got %+v\", snapshot)\n\t}\n}\n\nfunc TestReloadWithoutDirectivesSucceeds(t *testing.T) {\n\tengine, err := NewEngine(Config{\n\t\tRequestBodyAccess: true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"failed to initialize engine without directives: %v\", err)\n\t}\n\n\tif err := engine.Reload(); err != nil {\n\t\tt.Fatalf(\"expected reload without directives to succeed, got %v\", err)\n\t}\n\n\tsnapshot := engine.Snapshot()\n\tif snapshot.ReloadSuccessTotal != 1 {\n\t\tt.Fatalf(\"expected ReloadSuccessTotal=1, got %#v\", snapshot.ReloadSuccessTotal)\n\t}\n\tif snapshot.InitSuccessTotal != 1 {\n\t\tt.Fatalf(\"expected InitSuccessTotal=1, got %#v\", snapshot.InitSuccessTotal)\n\t}\n\tif snapshot.ReloadCount != 1 {\n\t\tt.Fatalf(\"expected ReloadCount=1, got %#v\", snapshot.ReloadCount)\n\t}\n}\n\nfunc TestNewEngineWildcardDirectivesRequireMatch(t *testing.T) {\n\t_, err := NewEngine(Config{\n\t\tDirectivesFile: []string{filepath.Join(t.TempDir(), \"*.conf\")},\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"expected wildcard directives with no matches to fail\")\n\t}\n\tif !strings.Contains(err.Error(), \"matched no files\") {\n\t\tt.Fatalf(\"expected wildcard match error, got %v\", err)\n\t}\n}\n\nfunc TestNewEngineQuestionMarkGlobDirectivesMatch(t *testing.T) {\n\trootDir := t.TempDir()\n\twriteRuleFile(t, rootDir, \"rule-1.conf\", testRules)\n\n\tengine, err := NewEngine(Config{\n\t\tDirectivesFile:    []string{filepath.Join(rootDir, \"rule-?.conf\")},\n\t\tRequestBodyAccess: true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"expected question mark glob to match directives file, got %v\", err)\n\t}\n\tif engine == nil || engine.waf == nil {\n\t\tt.Fatal(\"expected engine to initialize from question mark glob\")\n\t}\n}\n\nfunc TestNewEngineCharacterClassGlobDirectivesMatch(t *testing.T) {\n\trootDir := t.TempDir()\n\twriteRuleFile(t, rootDir, \"rule-1.conf\", testRules)\n\n\tengine, err := NewEngine(Config{\n\t\tDirectivesFile:    []string{filepath.Join(rootDir, \"rule-[12].conf\")},\n\t\tRequestBodyAccess: true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"expected character class glob to match directives file, got %v\", err)\n\t}\n\tif engine == nil || engine.waf == nil {\n\t\tt.Fatal(\"expected engine to initialize from character class glob\")\n\t}\n}\n\nfunc TestNewEngineWildcardDirectivesWithRootFSRequireMatch(t *testing.T) {\n\trootDir := t.TempDir()\n\t_, err := NewEngine(Config{\n\t\tDirectivesFile: []string{\"*.conf\"},\n\t\tRootFS:         os.DirFS(rootDir),\n\t})\n\tif err == nil {\n\t\tt.Fatal(\"expected RootFS wildcard directives with no matches to fail\")\n\t}\n\tif !strings.Contains(err.Error(), \"matched no files\") {\n\t\tt.Fatalf(\"expected wildcard match error, got %v\", err)\n\t}\n}\n\nfunc TestDefaultMetricsCollectorRecordLatencyUsesOnlineAverage(t *testing.T) {\n\tcollector := NewDefaultMetricsCollector().(*defaultMetricsCollector)\n\n\tcollector.RecordLatency(time.Millisecond)\n\tcollector.RecordLatency(3 * time.Millisecond)\n\tcollector.RecordLatency(-time.Millisecond)\n\n\tsnapshot := collector.GetMetrics()\n\tif snapshot == nil {\n\t\tt.Fatal(\"expected metrics snapshot\")\n\t}\n\tif collector.latencyCount != 2 {\n\t\tt.Fatalf(\"expected negative latency sample to be ignored, got %d\", collector.latencyCount)\n\t}\n\tif snapshot.AvgLatencyMs != 2 {\n\t\tt.Fatalf(\"expected average latency to be 2ms, got %v\", snapshot.AvgLatencyMs)\n\t}\n}\n\nfunc newInstanceApp(engine *Engine, cfg MiddlewareConfig) *fiber.App {\n\tapp := fiber.New()\n\tapp.Use(engine.Middleware(cfg))\n\tapp.All(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"ok\")\n\t})\n\treturn app\n}\n\nfunc newTestEngine(t *testing.T) (*Engine, error) {\n\tt.Helper()\n\treturn newTestEngineWithRules(t, testRules)\n}\n\nfunc newTestEngineWithRules(t *testing.T, rules string) (*Engine, error) {\n\tt.Helper()\n\n\tpath := writeRuleFile(t, t.TempDir(), \"test.conf\", rules)\n\treturn NewEngine(Config{\n\t\tLogLevel:          fiberlog.LevelInfo,\n\t\tDirectivesFile:    []string{path},\n\t\tRequestBodyAccess: true,\n\t})\n}\n\nfunc writeRuleFile(t *testing.T, dir, name, contents string) string {\n\tt.Helper()\n\n\tpath := filepath.Join(dir, name)\n\tif err := os.WriteFile(path, []byte(contents), 0o600); err != nil {\n\t\tt.Fatalf(\"failed to write directives file: %v\", err)\n\t}\n\n\treturn path\n}\n\nfunc performRequest(t *testing.T, app *fiber.App, req *http.Request) *http.Response {\n\tt.Helper()\n\n\tresp, err := app.Test(req)\n\tif err != nil {\n\t\tt.Fatalf(\"request failed: %v\", err)\n\t}\n\n\treturn resp\n}\n\ntype nilSnapshotCollector struct{}\n\nfunc (nilSnapshotCollector) RecordRequest()               {}\nfunc (nilSnapshotCollector) RecordBlock()                 {}\nfunc (nilSnapshotCollector) RecordLatency(time.Duration)  {}\nfunc (nilSnapshotCollector) GetMetrics() *MetricsSnapshot { return nil }\nfunc (nilSnapshotCollector) Reset()                       {}\n\ntype nilPtrSnapshotCollector struct{}\n\nfunc (*nilPtrSnapshotCollector) RecordRequest()               {}\nfunc (*nilPtrSnapshotCollector) RecordBlock()                 {}\nfunc (*nilPtrSnapshotCollector) RecordLatency(time.Duration)  {}\nfunc (*nilPtrSnapshotCollector) GetMetrics() *MetricsSnapshot { return nil }\nfunc (*nilPtrSnapshotCollector) Reset()                       {}\n\ntype countingCollector struct {\n\trequests uint64\n\tblocks   uint64\n}\n\nfunc (c *countingCollector) RecordRequest()              { c.requests++ }\nfunc (c *countingCollector) RecordBlock()                { c.blocks++ }\nfunc (c *countingCollector) RecordLatency(time.Duration) {}\nfunc (c *countingCollector) GetMetrics() *MetricsSnapshot {\n\treturn &MetricsSnapshot{\n\t\tTotalRequests:   c.requests,\n\t\tBlockedRequests: c.blocks,\n\t\tTimestamp:       time.Now(),\n\t}\n}\nfunc (c *countingCollector) Reset() {\n\tc.requests = 0\n\tc.blocks = 0\n}\n\ntype fakePanicWAF struct{}\n\nfunc (fakePanicWAF) NewTransaction() types.Transaction {\n\treturn fakePanicTransaction{}\n}\n\nfunc (fakePanicWAF) NewTransactionWithID(string) types.Transaction {\n\treturn fakePanicTransaction{}\n}\n\ntype fakePanicTransaction struct{}\n\nfunc (fakePanicTransaction) ProcessConnection(string, int, string, int)       {}\nfunc (fakePanicTransaction) ProcessURI(string, string, string)                {}\nfunc (fakePanicTransaction) SetServerName(string)                             {}\nfunc (fakePanicTransaction) AddRequestHeader(string, string)                  {}\nfunc (fakePanicTransaction) ProcessRequestHeaders() *types.Interruption       { panic(\"boom\") }\nfunc (fakePanicTransaction) RequestBodyReader() (io.Reader, error)            { return bytes.NewReader(nil), nil }\nfunc (fakePanicTransaction) AddGetRequestArgument(string, string)             {}\nfunc (fakePanicTransaction) AddPostRequestArgument(string, string)            {}\nfunc (fakePanicTransaction) AddPathRequestArgument(string, string)            {}\nfunc (fakePanicTransaction) AddResponseArgument(string, string)               {}\nfunc (fakePanicTransaction) ProcessRequestBody() (*types.Interruption, error) { return nil, nil }\nfunc (fakePanicTransaction) WriteRequestBody([]byte) (*types.Interruption, int, error) {\n\treturn nil, 0, nil\n}\nfunc (fakePanicTransaction) ReadRequestBodyFrom(io.Reader) (*types.Interruption, int, error) {\n\treturn nil, 0, nil\n}\nfunc (fakePanicTransaction) AddResponseHeader(string, string) {}\nfunc (fakePanicTransaction) ProcessResponseHeaders(int, string) *types.Interruption {\n\treturn nil\n}\nfunc (fakePanicTransaction) ResponseBodyReader() (io.Reader, error) { return bytes.NewReader(nil), nil }\nfunc (fakePanicTransaction) ProcessResponseBody() (*types.Interruption, error) {\n\treturn nil, nil\n}\nfunc (fakePanicTransaction) WriteResponseBody([]byte) (*types.Interruption, int, error) {\n\treturn nil, 0, nil\n}\nfunc (fakePanicTransaction) ReadResponseBodyFrom(io.Reader) (*types.Interruption, int, error) {\n\treturn nil, 0, nil\n}\nfunc (fakePanicTransaction) ProcessLogging()                {}\nfunc (fakePanicTransaction) IsRuleEngineOff() bool          { return false }\nfunc (fakePanicTransaction) IsRequestBodyAccessible() bool  { return false }\nfunc (fakePanicTransaction) IsResponseBodyAccessible() bool { return false }\nfunc (fakePanicTransaction) IsResponseBodyProcessable() bool {\n\treturn false\n}\nfunc (fakePanicTransaction) IsInterrupted() bool               { return false }\nfunc (fakePanicTransaction) Interruption() *types.Interruption { return nil }\nfunc (fakePanicTransaction) MatchedRules() []types.MatchedRule { return nil }\nfunc (fakePanicTransaction) DebugLogger() debuglog.Logger      { return nil }\nfunc (fakePanicTransaction) ID() string                        { return \"panic-tx\" }\nfunc (fakePanicTransaction) Close() error                      { return nil }\n"
  },
  {
    "path": "v3/coraza/go.mod",
    "content": "module github.com/gofiber/contrib/v3/coraza\n\ngo 1.26.2\n\nrequire (\n\tgithub.com/corazawaf/coraza/v3 v3.7.0\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/corazawaf/libinjection-go v0.3.2 // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/kaptinlin/go-i18n v0.4.0 // indirect\n\tgithub.com/kaptinlin/jsonpointer v0.4.18 // indirect\n\tgithub.com/kaptinlin/jsonschema v0.7.7 // indirect\n\tgithub.com/kaptinlin/messageformat-go v0.4.20 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/magefile/mage v1.17.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.2.0 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valllabh/ocsf-schema-golang v1.0.3 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\trsc.io/binaryregexp v0.2.0 // indirect\n)\n"
  },
  {
    "path": "v3/coraza/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=\ngithub.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU=\ngithub.com/corazawaf/coraza/v3 v3.7.0 h1:LIQqu1r+l6e/U/gyiZeykWaNNBY1TzRLz+aaI+QYEEM=\ngithub.com/corazawaf/coraza/v3 v3.7.0/go.mod h1:dOSt5evqC7EstouEv6ghhui01+oVUwp9X1vybWwqTlo=\ngithub.com/corazawaf/libinjection-go v0.3.2 h1:9rrKt0lpg4WvUXt+lwS06GywfqRXXsa/7JcOw5cQLwI=\ngithub.com/corazawaf/libinjection-go v0.3.2/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=\ngithub.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=\ngithub.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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/jcchavezs/mergefs v0.1.1 h1:D45R17m6dHnSVZefnhynoeZvcK2Uw0oTrRfoUOQ0S5Y=\ngithub.com/jcchavezs/mergefs v0.1.1/go.mod h1:eRLTrsA+vFwQZ48hj8p8gki/5v9C2bFtHH5Mnn4bcGk=\ngithub.com/kaptinlin/go-i18n v0.4.0 h1:i7L3U2yurg+xhokITtJ0k+mjHnXqkoyz8ju5Wb7W8Oc=\ngithub.com/kaptinlin/go-i18n v0.4.0/go.mod h1:njA6x0+4MWGcLWT0KLrwekhRPmze1Hnstf2+VJFzwpM=\ngithub.com/kaptinlin/jsonpointer v0.4.18 h1:EDUXT4WKpOKguU7oaFv6VaNatN7uHFe6dEYHX0+OFxs=\ngithub.com/kaptinlin/jsonpointer v0.4.18/go.mod h1:ndmfvrqrEDSbV3F7yGaOuDvr29WrxYU1aqkvef9L2do=\ngithub.com/kaptinlin/jsonschema v0.7.7 h1:41BlQJ9dskH0oE5DSzBUrl/w4JQYIr6N6L0B5GNyDoM=\ngithub.com/kaptinlin/jsonschema v0.7.7/go.mod h1:rKjWfyySHSxAD7Li2ctYkPlOu960igoKBvZ2ADRtd5Q=\ngithub.com/kaptinlin/messageformat-go v0.4.20 h1:a0ufTd5liiUubIGeGxpSTnNS8ZSrN4DV01/wGFmfzMs=\ngithub.com/kaptinlin/messageformat-go v0.4.20/go.mod h1:FqdEPfQLkqVBX7OBRMPgYwUPvKYJohFD9Ok1BMzCfIo=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/magefile/mage v1.17.1 h1:F1d2lnLSlbQDM0Plq6Ac4NtaHxkxTK8t5nrMY9SkoNA=\ngithub.com/magefile/mage v1.17.1/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=\ngithub.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=\ngithub.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=\ngithub.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI=\ngithub.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=\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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=\ngithub.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=\ngithub.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=\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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nrsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\n"
  },
  {
    "path": "v3/coraza/metrics.go",
    "content": "// Package coraza includes lightweight metrics and lifecycle snapshots for Engine instances.\npackage coraza\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// MetricsCollector records lightweight request metrics for a Coraza Engine.\ntype MetricsCollector interface {\n\tRecordRequest()\n\tRecordBlock()\n\tRecordLatency(duration time.Duration)\n\tGetMetrics() *MetricsSnapshot\n\tReset()\n}\n\n// MetricsSnapshot represents the current request metrics for an Engine.\ntype MetricsSnapshot struct {\n\t// TotalRequests is the number of requests observed by the middleware.\n\tTotalRequests uint64 `json:\"total_requests\"`\n\t// BlockedRequests is the number of requests interrupted by the WAF.\n\tBlockedRequests uint64 `json:\"blocked_requests\"`\n\t// AvgLatencyMs is the average middleware latency in milliseconds.\n\tAvgLatencyMs float64 `json:\"avg_latency_ms\"`\n\t// BlockRate is the ratio of blocked requests to total requests.\n\tBlockRate float64 `json:\"block_rate\"`\n\t// Timestamp is when the snapshot was generated.\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\n// EngineSnapshot represents lifecycle and configuration state for an Engine.\ntype EngineSnapshot struct {\n\t// Initialized reports whether the Engine currently holds a usable WAF instance.\n\tInitialized bool `json:\"initialized\"`\n\t// SupportsOptions reports whether the current WAF supports Coraza experimental options.\n\tSupportsOptions bool `json:\"supports_options\"`\n\t// ConfigFiles lists the directive files for the active configuration.\n\tConfigFiles []string `json:\"config_files\"`\n\t// LastAttemptConfigFiles lists the directive files from the most recent init attempt.\n\tLastAttemptConfigFiles []string `json:\"last_attempt_config_files\"`\n\t// LastInitError contains the most recent initialization error, if any.\n\tLastInitError string `json:\"last_init_error,omitempty\"`\n\t// LastLoadedAt is the timestamp of the most recent successful initialization or reload.\n\tLastLoadedAt time.Time `json:\"last_loaded_at\"`\n\t// InitSuccessTotal is the number of successful init calls.\n\tInitSuccessTotal uint64 `json:\"init_success_total\"`\n\t// InitFailureTotal is the number of failed init calls.\n\tInitFailureTotal uint64 `json:\"init_failure_total\"`\n\t// ReloadSuccessTotal is the number of successful reload calls.\n\tReloadSuccessTotal uint64 `json:\"reload_success_total\"`\n\t// ReloadFailureTotal is the number of failed reload calls.\n\tReloadFailureTotal uint64 `json:\"reload_failure_total\"`\n\t// ReloadCount is the total number of successful reload transitions.\n\tReloadCount uint64 `json:\"reload_count\"`\n}\n\n// MetricsReport combines request metrics with Engine lifecycle information.\ntype MetricsReport struct {\n\t// Requests is the request metrics snapshot.\n\tRequests MetricsSnapshot `json:\"requests\"`\n\t// Engine is the Engine lifecycle snapshot.\n\tEngine EngineSnapshot `json:\"engine\"`\n}\n\ntype defaultMetricsCollector struct {\n\ttotalRequests   atomic.Uint64\n\tblockedRequests atomic.Uint64\n\n\tlatencyMutex sync.RWMutex\n\tavgLatencyNs float64\n\tlatencyCount uint64\n}\n\n// NewDefaultMetricsCollector creates the built-in in-memory metrics collector.\nfunc NewDefaultMetricsCollector() MetricsCollector {\n\treturn &defaultMetricsCollector{}\n}\n\nfunc (m *defaultMetricsCollector) RecordRequest() {\n\tm.totalRequests.Add(1)\n}\n\nfunc (m *defaultMetricsCollector) RecordBlock() {\n\tm.blockedRequests.Add(1)\n}\n\nfunc (m *defaultMetricsCollector) RecordLatency(duration time.Duration) {\n\tif duration < 0 {\n\t\treturn\n\t}\n\n\tm.latencyMutex.Lock()\n\tdefer m.latencyMutex.Unlock()\n\n\tm.latencyCount++\n\tcount := float64(m.latencyCount)\n\tm.avgLatencyNs += (float64(duration.Nanoseconds()) - m.avgLatencyNs) / count\n}\n\nfunc (m *defaultMetricsCollector) GetMetrics() *MetricsSnapshot {\n\ttotalReqs := m.totalRequests.Load()\n\tblockedReqs := m.blockedRequests.Load()\n\n\tm.latencyMutex.RLock()\n\tvar avgLatencyMs float64\n\tif m.latencyCount > 0 {\n\t\tavgLatencyMs = m.avgLatencyNs / 1e6\n\t}\n\tm.latencyMutex.RUnlock()\n\n\tvar blockRate float64\n\tif totalReqs > 0 {\n\t\tblockRate = float64(blockedReqs) / float64(totalReqs)\n\t}\n\n\treturn &MetricsSnapshot{\n\t\tTotalRequests:   totalReqs,\n\t\tBlockedRequests: blockedReqs,\n\t\tAvgLatencyMs:    avgLatencyMs,\n\t\tBlockRate:       blockRate,\n\t\tTimestamp:       time.Now(),\n\t}\n}\n\nfunc (m *defaultMetricsCollector) Reset() {\n\tm.totalRequests.Store(0)\n\tm.blockedRequests.Store(0)\n\n\tm.latencyMutex.Lock()\n\tdefer m.latencyMutex.Unlock()\n\tm.avgLatencyNs = 0\n\tm.latencyCount = 0\n}\n\n// MetricsSnapshot returns a copy of the Engine's current request metrics.\nfunc (e *Engine) MetricsSnapshot() MetricsSnapshot {\n\tcollector := e.Metrics()\n\tif collector == nil {\n\t\treturn MetricsSnapshot{Timestamp: time.Now()}\n\t}\n\n\tsnapshot := collector.GetMetrics()\n\tif snapshot == nil {\n\t\treturn MetricsSnapshot{Timestamp: time.Now()}\n\t}\n\n\treturn *snapshot\n}\n\n// Snapshot returns lifecycle and configuration state for the Engine.\nfunc (e *Engine) Snapshot() EngineSnapshot {\n\treturn e.observabilitySnapshot()\n}\n\n// Report returns both the request metrics and lifecycle snapshot for the Engine.\nfunc (e *Engine) Report() MetricsReport {\n\treturn MetricsReport{\n\t\tRequests: e.MetricsSnapshot(),\n\t\tEngine:   e.Snapshot(),\n\t}\n}\n"
  },
  {
    "path": "v3/fgprof/README.md",
    "content": "---\nid: fgprof\n---\n\n# Fgprof\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*fgprof*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20Fgprof/badge.svg)\n\n[fgprof](https://github.com/felixge/fgprof) support for Fiber.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\nUsing fgprof to profiling your Fiber app.\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/fgprof\n```\n\n## Config\n\n| Property | Type                      | Description                                                                                                                                      | Default |\n|----------|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|---------|\n| Next     | `func(c fiber.Ctx) bool` | A function to skip this middleware when returned `true`.                                                                                         | `nil`   |\n| Prefix   | `string`.                 | Prefix defines a URL prefix added before \"/debug/fgprof\". Note that it should start with (but not end with) a slash. Example: \"/federated-fiber\" | `\"\"`    |\n\n## Example\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/contrib/v3/fgprof\"\n    \"github.com/gofiber/fiber/v3\"\n)\n\nfunc main() {\n    app := fiber.New()\n    app.Use(fgprof.New())\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"OK\")\n    })\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n```bash\ngo tool pprof -http=:8080 http://localhost:3000/debug/fgprof\n```\n"
  },
  {
    "path": "v3/fgprof/config.go",
    "content": "package fgprof\n\nimport \"github.com/gofiber/fiber/v3\"\n\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 is the path where the fprof endpoints will be mounted.\n\t// Default Path is \"/debug/fgprof\"\n\t//\n\t// Optional. Default: \"\"\n\tPrefix string\n}\n\n// ConfigDefault is the default config\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": "v3/fgprof/fgprof.go",
    "content": "package fgprof\n\nimport (\n\t\"github.com/felixge/fgprof\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/adaptor\"\n)\n\nfunc New(conf ...Config) fiber.Handler {\n\t// Set default config\n\tcfg := configDefault(conf...)\n\n\tfgProfPath := cfg.Prefix + \"/debug/fgprof\"\n\n\tvar fgprofHandler = adaptor.HTTPHandler(fgprof.Handler())\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\tif c.Path() == fgProfPath {\n\t\t\treturn fgprofHandler(c)\n\t\t}\n\t\treturn c.Next()\n\t}\n}\n"
  },
  {
    "path": "v3/fgprof/fgprof_test.go",
    "content": "package fgprof\n\nimport (\n\t\"io\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\n// go test -run Test_Non_Fgprof_Path\nfunc Test_Non_Fgprof_Path(t *testing.T) {\n\tapp := fiber.New()\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(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"escaped\", string(body))\n}\n\n// go test -run Test_Non_Fgprof_Path_WithPrefix\nfunc Test_Non_Fgprof_Path_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrefix: \"/prefix\",\n\t}))\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(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, \"escaped\", string(body))\n}\n\n// go test -run Test_Fgprof_Path\nfunc Test_Fgprof_Path(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New())\n\n\t// Default fgprof interval is 30 seconds\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/debug/fgprof?seconds=1\", nil), fiber.TestConfig{Timeout: 3000})\n\tif err != nil && strings.Contains(err.Error(), \"empty response\") {\n\t\tt.Skip(\"fiber test helper returns empty response for streaming endpoints\")\n\t}\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Fgprof_Path_WithPrefix\nfunc Test_Fgprof_Path_WithPrefix(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrefix: \"/test\",\n\t}))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"escaped\")\n\t})\n\n\t// Non fgprof prefix path\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/prefix/debug/fgprof?seconds=1\", nil), fiber.TestConfig{Timeout: 3000})\n\tif err != nil && strings.Contains(err.Error(), \"empty response\") {\n\t\tt.Skip(\"fiber test helper returns empty response for streaming endpoints\")\n\t}\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 404, resp.StatusCode)\n\t// Fgprof prefix path\n\tresp, err = app.Test(httptest.NewRequest(\"GET\", \"/test/debug/fgprof?seconds=1\", nil), fiber.TestConfig{Timeout: 3000})\n\tif err != nil && strings.Contains(err.Error(), \"empty response\") {\n\t\tt.Skip(\"fiber test helper returns empty response for streaming endpoints\")\n\t}\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n}\n\n// go test -run Test_Fgprof_Next\nfunc Test_Fgprof_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/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 404, resp.StatusCode)\n}\n\n// go test -run Test_Fgprof_Next_WithPrefix\nfunc Test_Fgprof_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/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 404, resp.StatusCode)\n}\n"
  },
  {
    "path": "v3/fgprof/go.mod",
    "content": "module github.com/gofiber/contrib/v3/fgprof\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/felixge/fgprof v0.9.5\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/fgprof/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=\ngithub.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=\ngithub.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=\ngithub.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=\ngithub.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0=\ngithub.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=\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/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\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": "v3/hcaptcha/README.md",
    "content": "---\nid: hcaptcha\n---\n\n# HCaptcha\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*hcaptcha*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20hcaptcha/badge.svg)\n\nA simple [HCaptcha](https://hcaptcha.com) middleware to prevent bot attacks.\n\n:::note\n\nRequires Go **1.25** and above\n\n:::\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n:::caution\n\nThis middleware only supports Fiber **v3**.\n\n:::\n\n```shell\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/hcaptcha\n```\n\n## Signature\n\n```go\nhcaptcha.New(config hcaptcha.Config) fiber.Handler\n```\n\n## Config\n\n| Property        | Type                               | Description                                                                                                                                                                                                                                                                                  | Default                               |\n|:----------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------|\n| SecretKey       | `string`                           | The secret key you obtained from the HCaptcha admin panel. This field must not be empty.                                                                                                                                                                                                    | `\"\"`                                  |\n| ResponseKeyFunc | `func(fiber.Ctx) (string, error)`  | ResponseKeyFunc should return the token that the captcha provides upon successful solving. By default, it gets the token from the body by parsing a JSON request and returns the `hcaptcha_token` field.                                                                                 | `hcaptcha.DefaultResponseKeyFunc`     |\n| SiteVerifyURL   | `string`                           | This property specifies the API resource used for token authentication.                                                                                                                                                                                                                      | `https://api.hcaptcha.com/siteverify` |\n| ValidateFunc    | `func(success bool, c fiber.Ctx) error` | Optional custom validation hook called after siteverify completes. Parameters: `success` (hCaptcha verification result), `c` (Fiber context). Return `nil` to continue, or return an `error` to stop request processing. If unset, middleware defaults to blocking unsuccessful verification. For secure bot protection, reject when `success == false`. | `nil`                                 |\n\n## Example\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"log\"\n\n\t\"github.com/gofiber/contrib/v3/hcaptcha\"\n\t\"github.com/gofiber/fiber/v3\"\n)\n\nconst (\n    TestSecretKey = \"0x0000000000000000000000000000000000000000\"\n    TestSiteKey   = \"20000000-ffff-ffff-ffff-000000000002\"\n)\n\nfunc main() {\n\tapp := fiber.New()\n\tcaptcha := hcaptcha.New(hcaptcha.Config{\n\t\t// Must set the secret key.\n\t\tSecretKey: TestSecretKey,\n\t\t// Optional custom validation handling.\n\t\tValidateFunc: func(success bool, c fiber.Ctx) error {\n\t\t\tif !success {\n\t\t\t\tif err := c.Status(fiber.StatusForbidden).JSON(fiber.Map{\n\t\t\t\t\t\"error\":   \"HCaptcha validation failed\",\n\t\t\t\t\t\"details\": \"Please complete the captcha challenge and try again\",\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn errors.New(\"custom validation failed\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t})\n\n\tapp.Get(\"/api/\", func(c fiber.Ctx) error {\n\t\treturn c.JSON(fiber.Map{\n\t\t\t\"hcaptcha_site_key\": TestSiteKey,\n\t\t})\n\t})\n\n\t// Middleware order matters: place hcaptcha middleware before the final handler.\n\tapp.Post(\"/api/submit\", captcha, func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"You are not a robot\")\n\t})\n\n\tlog.Fatal(app.Listen(\":3000\"))\n}\n```\n"
  },
  {
    "path": "v3/hcaptcha/config.go",
    "content": "package hcaptcha\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n)\n\n// DefaultSiteVerifyURL is the default URL for the HCaptcha API\nconst DefaultSiteVerifyURL = \"https://api.hcaptcha.com/siteverify\"\n\n// Config defines the config for HCaptcha middleware.\ntype Config struct {\n\t// SecretKey is the secret key you get from HCaptcha when you create a new application\n\tSecretKey string\n\t// ResponseKeyFunc should return the generated pass UUID from the ctx, which will be validated\n\tResponseKeyFunc func(fiber.Ctx) (string, error)\n\t// SiteVerifyURL is the endpoint URL where the program should verify the given token\n\t// default value is: \"https://api.hcaptcha.com/siteverify\"\n\tSiteVerifyURL string\n\t// ValidateFunc allows custom validation handling based on the HCaptcha validation result.\n\t// If set, it is called with the API success status and the current context after siteverify.\n\t// For secure bot protection, reject requests when success is false.\n\t// Return nil to continue to the next handler, or return an error to stop the middleware chain.\n\t// If ValidateFunc is nil, default behavior is used and unsuccessful verification returns 403.\n\tValidateFunc func(success bool, c fiber.Ctx) error\n}\n\n// DefaultResponseKeyFunc is the default function to get the HCaptcha token from the request body\nfunc DefaultResponseKeyFunc(c fiber.Ctx) (string, error) {\n\tdata := struct {\n\t\tHCaptchaToken string `json:\"hcaptcha_token\"`\n\t}{}\n\n\terr := json.NewDecoder(bytes.NewReader(c.Body())).Decode(&data)\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode HCaptcha token: %w\", err)\n\t}\n\n\tif utils.TrimSpace(data.HCaptchaToken) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"hcaptcha token is empty\")\n\t}\n\n\treturn data.HCaptchaToken, nil\n}\n"
  },
  {
    "path": "v3/hcaptcha/go.mod",
    "content": "module github.com/gofiber/contrib/v3/hcaptcha\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/valyala/fasthttp v1.70.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/hcaptcha/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/hcaptcha/hcaptcha.go",
    "content": "// Package hcaptcha is a simple middleware that checks for an HCaptcha UUID\n// and then validates it. It returns an error if the UUID is not valid (the request may have been sent by a robot).\npackage hcaptcha\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/valyala/fasthttp\"\n\t\"net/url\"\n)\n\n// HCaptcha is a middleware handler that checks for an HCaptcha UUID and then validates it.\ntype HCaptcha struct {\n\tConfig\n}\n\n// New creates a new HCaptcha middleware handler.\nfunc New(config Config) fiber.Handler {\n\tif config.SiteVerifyURL == \"\" {\n\t\tconfig.SiteVerifyURL = DefaultSiteVerifyURL\n\t}\n\n\tif config.ResponseKeyFunc == nil {\n\t\tconfig.ResponseKeyFunc = DefaultResponseKeyFunc\n\t}\n\n\th := &HCaptcha{\n\t\tconfig,\n\t}\n\treturn h.Validate\n}\n\n// Validate checks for an HCaptcha UUID and then validates it.\nfunc (h *HCaptcha) Validate(c fiber.Ctx) error {\n\ttoken, err := h.ResponseKeyFunc(c)\n\tif err != nil {\n\t\tc.Status(fiber.StatusBadRequest)\n\t\treturn fmt.Errorf(\"error retrieving HCaptcha token: %w\", err)\n\t}\n\n\treq := fasthttp.AcquireRequest()\n\tdefer fasthttp.ReleaseRequest(req)\n\treq.SetBody([]byte(url.Values{\n\t\t\"secret\":   {h.SecretKey},\n\t\t\"response\": {token},\n\t}.Encode()))\n\treq.Header.SetMethod(\"POST\")\n\treq.Header.SetContentType(\"application/x-www-form-urlencoded; charset=UTF-8\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.SetRequestURI(h.SiteVerifyURL)\n\tres := fasthttp.AcquireResponse()\n\tdefer fasthttp.ReleaseResponse(res)\n\n\t// Send the request to the HCaptcha API\n\tif err = fasthttp.Do(req, res); err != nil {\n\t\tc.Status(fiber.StatusBadRequest)\n\t\treturn fmt.Errorf(\"error sending request to HCaptcha API: %w\", err)\n\t}\n\n\to := struct {\n\t\tSuccess bool `json:\"success\"`\n\t}{}\n\n\tif err = json.NewDecoder(bytes.NewReader(res.Body())).Decode(&o); err != nil {\n\t\tc.Status(fiber.StatusInternalServerError)\n\t\treturn fmt.Errorf(\"error decoding HCaptcha API response: %w\", err)\n\t}\n\n\t// Execute custom validation if ValidateFunc is defined.\n\t// ValidateFunc receives the siteverify result and should return an error on validation failure.\n\t// If ValidateFunc is nil, default behavior rejects unsuccessful verification.\n\tvar validationErr error\n\tif h.ValidateFunc != nil {\n\t\tvalidationErr = h.ValidateFunc(o.Success, c)\n\t} else if !o.Success {\n\t\tvalidationErr = errors.New(\"unable to check that you are not a robot\")\n\t}\n\n\tif validationErr != nil {\n\t\tstatusCode := c.Response().StatusCode()\n\t\tif statusCode == 0 || statusCode == fiber.StatusOK {\n\t\t\tc.Status(fiber.StatusForbidden)\n\t\t}\n\t\tif len(c.Response().Body()) == 0 {\n\t\t\treturn c.SendString(validationErr.Error())\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn c.Next()\n}\n"
  },
  {
    "path": "v3/hcaptcha/hcaptcha_test.go",
    "content": "package hcaptcha\n\nimport (\n\t\"errors\"\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/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tTestSecretKey     = \"0x0000000000000000000000000000000000000000\"\n\tTestResponseToken = \"20000000-aaaa-bbbb-cccc-000000000002\"\n)\n\nfunc newSiteVerifyServer(t *testing.T, success bool) *httptest.Server {\n\tt.Helper()\n\n\treturn httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tassert.Equal(t, http.MethodPost, r.Method)\n\t\tassert.Equal(t, \"application/json\", r.Header.Get(\"Accept\"))\n\t\t_, err := io.ReadAll(r.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, r.Body.Close())\n\n\t\t_, err = w.Write([]byte(`{\"success\":` + map[bool]string{true: \"true\", false: \"false\"}[success] + `}`))\n\t\trequire.NoError(t, err)\n\t}))\n}\n\nfunc TestHCaptchaDefaultValidation(t *testing.T) {\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tserver := newSiteVerifyServer(t, true)\n\t\tdefer server.Close()\n\n\t\tapp := fiber.New()\n\t\tm := New(Config{\n\t\t\tSecretKey:     TestSecretKey,\n\t\t\tSiteVerifyURL: server.URL,\n\t\t\tResponseKeyFunc: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn TestResponseToken, nil\n\t\t\t},\n\t\t})\n\n\t\tapp.Get(\"/hcaptcha\", m, func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/hcaptcha\", nil)\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, fiber.StatusOK, res.StatusCode)\n\t\tassert.Equal(t, \"ok\", string(body))\n\t})\n\n\tt.Run(\"failure\", func(t *testing.T) {\n\t\tserver := newSiteVerifyServer(t, false)\n\t\tdefer server.Close()\n\n\t\tapp := fiber.New()\n\t\tm := New(Config{\n\t\t\tSecretKey:     TestSecretKey,\n\t\t\tSiteVerifyURL: server.URL,\n\t\t\tResponseKeyFunc: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn TestResponseToken, nil\n\t\t\t},\n\t\t})\n\n\t\tapp.Get(\"/hcaptcha\", m, func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/hcaptcha\", nil)\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusForbidden, res.StatusCode)\n\t\tassert.Equal(t, \"unable to check that you are not a robot\", string(body))\n\t})\n}\n\nfunc TestHCaptchaValidateFunc(t *testing.T) {\n\tt.Run(\"called with success and allows request\", func(t *testing.T) {\n\t\tserver := newSiteVerifyServer(t, true)\n\t\tdefer server.Close()\n\n\t\tapp := fiber.New()\n\t\tcalled := false\n\t\tm := New(Config{\n\t\t\tSecretKey:     TestSecretKey,\n\t\t\tSiteVerifyURL: server.URL,\n\t\t\tResponseKeyFunc: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn TestResponseToken, nil\n\t\t\t},\n\t\t\tValidateFunc: func(success bool, c fiber.Ctx) error {\n\t\t\t\tcalled = true\n\t\t\t\tassert.True(t, success)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\n\t\tapp.Get(\"/hcaptcha\", m, func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/hcaptcha\", nil)\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tassert.Equal(t, fiber.StatusOK, res.StatusCode)\n\t\tassert.True(t, called)\n\t})\n\n\tt.Run(\"custom status is preserved on validation error\", func(t *testing.T) {\n\t\tserver := newSiteVerifyServer(t, false)\n\t\tdefer server.Close()\n\n\t\tapp := fiber.New()\n\t\tm := New(Config{\n\t\t\tSecretKey:     TestSecretKey,\n\t\t\tSiteVerifyURL: server.URL,\n\t\t\tResponseKeyFunc: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn TestResponseToken, nil\n\t\t\t},\n\t\t\tValidateFunc: func(success bool, c fiber.Ctx) error {\n\t\t\t\tassert.False(t, success)\n\t\t\t\tc.Status(fiber.StatusUnprocessableEntity)\n\t\t\t\treturn errors.New(\"custom validation failed\")\n\t\t\t},\n\t\t})\n\n\t\tapp.Get(\"/hcaptcha\", m, func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/hcaptcha\", nil)\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusUnprocessableEntity, res.StatusCode)\n\t\tassert.Equal(t, \"custom validation failed\", string(body))\n\t})\n\n\tt.Run(\"defaults to 403 and error body when validatefunc sets neither\", func(t *testing.T) {\n\t\tserver := newSiteVerifyServer(t, false)\n\t\tdefer server.Close()\n\n\t\tapp := fiber.New()\n\t\tm := New(Config{\n\t\t\tSecretKey:     TestSecretKey,\n\t\t\tSiteVerifyURL: server.URL,\n\t\t\tResponseKeyFunc: func(c fiber.Ctx) (string, error) {\n\t\t\t\treturn TestResponseToken, nil\n\t\t\t},\n\t\t\tValidateFunc: func(success bool, c fiber.Ctx) error {\n\t\t\t\tassert.False(t, success)\n\t\t\t\treturn errors.New(\"custom validation failed\")\n\t\t\t},\n\t\t})\n\n\t\tapp.Get(\"/hcaptcha\", m, func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"ok\")\n\t\t})\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"/hcaptcha\", nil)\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusForbidden, res.StatusCode)\n\t\tassert.Equal(t, \"custom validation failed\", string(body))\n\t})\n}\n\nfunc TestDefaultResponseKeyFunc(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Post(\"/\", func(c fiber.Ctx) error {\n\t\ttoken, err := DefaultResponseKeyFunc(c)\n\t\tif err != nil {\n\t\t\treturn c.Status(fiber.StatusBadRequest).SendString(err.Error())\n\t\t}\n\n\t\treturn c.SendString(token)\n\t})\n\n\tt.Run(\"valid token\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(`{\"hcaptcha_token\":\"abc\"}`))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusOK, res.StatusCode)\n\t\tassert.Equal(t, \"abc\", string(body))\n\t})\n\n\tt.Run(\"empty token\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodPost, \"/\", strings.NewReader(`{\"hcaptcha_token\":\"  \"}`))\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\trequire.NoError(t, err)\n\t\tdefer res.Body.Close()\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusBadRequest, res.StatusCode)\n\t\tassert.Equal(t, \"hcaptcha token is empty\", string(body))\n\t})\n}\n"
  },
  {
    "path": "v3/i18n/README.md",
    "content": "---\nid: i18n\n---\n\n# I18n\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*i18n*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20i18n/badge.svg)\n\n[go-i18n](https://github.com/nicksnyder/go-i18n) support for Fiber.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/i18n\n```\n\n## API\n\n| Name                 | Signature                                                                | Description                                                                 |\n|----------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------|\n| New                  | `New(config ...*i18n.Config) *i18n.I18n`                                 | Create a reusable, thread-safe localization container.                     |\n| (*I18n).Localize     | `Localize(ctx fiber.Ctx, params interface{}) (string, error)`            | Returns a localized message. `params` must be a message ID string or `*goi18n.LocalizeConfig`. Returns an error if the message is not found, the param type is unsupported, or `params` is nil. |\n| (*I18n).MustLocalize | `MustLocalize(ctx fiber.Ctx, params interface{}) string`                 | Like `Localize` but panics on any error.                                    |\n\n## Types\n\n| Name             | Description                                                                                         |\n|------------------|-----------------------------------------------------------------------------------------------------|\n| `Loader`         | Interface for loading message files. Implement `LoadMessage(path string) ([]byte, error)`.         |\n| `LoaderFunc`     | Adapter to use a plain function as a `Loader`.                                                     |\n| `EmbedLoader`    | `Loader` implementation backed by an `embed.FS`. Use with Go's `//go:embed` directive.             |\n\n## Config\n\n| Property         | Type                                              | Description                                                                                                                        | Default                                                                        |\n|------------------|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|\n| RootPath         | `string`                                          | The i18n template folder path.                                                                                                     | `\"./example/localize\"`                                                         |\n| AcceptLanguages  | `[]language.Tag`                                  | A collection of languages that can be processed.                                                                                   | `[]language.Tag{language.Chinese, language.English}`                           |\n| FormatBundleFile | `string`                                          | The type of the template file.                                                                                                     | `\"yaml\"`                                                                       |\n| DefaultLanguage  | `language.Tag`                                    | The default returned language type.                                                                                                | `language.English`                                                             |\n| Loader           | `Loader`                                          | The implementation of the Loader interface, which defines how to read the file. We provide both os.ReadFile and embed.FS.ReadFile. | `LoaderFunc(os.ReadFile)`                                                      |\n| UnmarshalFunc    | `i18n.UnmarshalFunc`                              | The function used for decoding template files.                                                                                     | `yaml.Unmarshal`                                                               |\n| LangHandler      | `func(ctx fiber.Ctx, defaultLang string) string` | Used to get the kind of language handled by fiber.Ctx and defaultLang.                                                            | Retrieved from the request header `Accept-Language` or query parameter `lang`. |\n\n## Example\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    contribi18n \"github.com/gofiber/contrib/v3/i18n\"\n    \"github.com/gofiber/fiber/v3\"\n    goi18n \"github.com/nicksnyder/go-i18n/v2/i18n\"\n    \"golang.org/x/text/language\"\n)\n\nfunc main() {\n    translator := contribi18n.New(&contribi18n.Config{\n        RootPath:        \"./example/localize\",\n        AcceptLanguages: []language.Tag{language.Chinese, language.English},\n        DefaultLanguage: language.Chinese,\n    })\n\n    app := fiber.New()\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        localize, err := translator.Localize(c, \"welcome\")\n        if err != nil {\n            return c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n        }\n        return c.SendString(localize)\n    })\n    app.Get(\"/:name\", func(ctx fiber.Ctx) error {\n        return ctx.SendString(translator.MustLocalize(ctx, &goi18n.LocalizeConfig{\n            MessageID: \"welcomeWithName\",\n            TemplateData: map[string]string{\n                \"name\": ctx.Params(\"name\"),\n            },\n        }))\n    })\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## Migration from middleware usage\n\nThe package now exposes a global, thread-safe container instead of middleware. To migrate existing code:\n\n1. Remove any `app.Use(i18n.New(...))` calls—the translator no longer registers middleware.\n2. Instantiate a shared translator during application startup with `translator := i18n.New(...)`.\n3. Replace package-level calls such as `i18n.Localize`/`i18n.MustLocalize` with the respective methods on your translator (`translator.Localize`, `translator.MustLocalize`).\n4. Drop any manual interaction with `ctx.Locals(\"i18n\")`; all state is managed inside the translator instance.\n\nThe translator instance is safe for concurrent use across handlers and reduces per-request allocations by reusing the same bundle and localizer map.\n"
  },
  {
    "path": "v3/i18n/config.go",
    "content": "package i18n\n\nimport (\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/nicksnyder/go-i18n/v2/i18n\"\n\t\"golang.org/x/text/language\"\n\t\"gopkg.in/yaml.v2\"\n)\n\ntype Config struct {\n\t// RootPath is i18n template folder path\n\t//\n\t// Default: ./example/localize\n\tRootPath string\n\n\t// AcceptLanguages is a collection of languages that can be processed\n\t//\n\t// Optional. Default: []language.Tag{language.Chinese, language.English}\n\tAcceptLanguages []language.Tag\n\n\t// FormatBundleFile is type of template file.\n\t//\n\t// Optional. Default: \"yaml\"\n\tFormatBundleFile string\n\n\t// DefaultLanguage is the default returned language type\n\t//\n\t// Optional. Default: language.English\n\tDefaultLanguage language.Tag\n\n\t// Loader implements the Loader interface, which defines how to read the file.\n\t// We provide both os.ReadFile and embed.FS.ReadFile\n\t// Optional. Default: LoaderFunc(os.ReadFile)\n\tLoader Loader\n\n\t// UnmarshalFunc for decoding template files\n\t//\n\t// Optional. Default: yaml.Unmarshal\n\tUnmarshalFunc i18n.UnmarshalFunc\n\n\t// LangHandler is used to get the kind of language handled by fiber.Ctx and defaultLang\n\t//\n\t// Optional. Default: The language type is retrieved from the request header: `Accept-Language` or query param : `lang`\n\tLangHandler func(ctx fiber.Ctx, defaultLang string) string\n\n\tbundle       *i18n.Bundle\n\tlocalizerMap *sync.Map\n}\n\ntype Loader interface {\n\tLoadMessage(path string) ([]byte, error)\n}\n\ntype LoaderFunc func(path string) ([]byte, error)\n\nfunc (f LoaderFunc) LoadMessage(path string) ([]byte, error) {\n\treturn f(path)\n}\n\nvar ConfigDefault = &Config{\n\tRootPath:         \"./example/localize\",\n\tDefaultLanguage:  language.English,\n\tAcceptLanguages:  []language.Tag{language.Chinese, language.English},\n\tFormatBundleFile: \"yaml\",\n\tUnmarshalFunc:    yaml.Unmarshal,\n\tLoader:           LoaderFunc(os.ReadFile),\n\tLangHandler:      defaultLangHandler,\n}\n\nfunc defaultLangHandler(c fiber.Ctx, defaultLang string) string {\n\tif c == nil || c.Request() == nil {\n\t\treturn defaultLang\n\t}\n\tif lang := c.Query(\"lang\"); lang != \"\" {\n\t\treturn utils.CopyString(lang)\n\t}\n\tif lang := c.Get(\"Accept-Language\"); lang != \"\" {\n\t\treturn utils.CopyString(lang)\n\t}\n\n\treturn defaultLang\n}\n\nfunc configDefault(config ...*Config) *Config {\n\tvar cfg *Config\n\n\tswitch {\n\tcase len(config) == 0 || config[0] == nil:\n\t\tcopyCfg := *ConfigDefault\n\t\t// ensure mutable fields are not shared with defaults\n\t\tif copyCfg.AcceptLanguages != nil {\n\t\t\tcopyCfg.AcceptLanguages = append([]language.Tag(nil), copyCfg.AcceptLanguages...)\n\t\t}\n\t\tcfg = &copyCfg\n\tdefault:\n\t\tcopyCfg := *config[0]\n\t\tif config[0].AcceptLanguages != nil {\n\t\t\tcopyCfg.AcceptLanguages = append([]language.Tag(nil), config[0].AcceptLanguages...)\n\t\t}\n\t\tcfg = &copyCfg\n\t}\n\n\tif cfg.RootPath == \"\" {\n\t\tcfg.RootPath = ConfigDefault.RootPath\n\t}\n\n\tif cfg.DefaultLanguage == language.Und {\n\t\tcfg.DefaultLanguage = ConfigDefault.DefaultLanguage\n\t}\n\n\tif cfg.FormatBundleFile == \"\" {\n\t\tcfg.FormatBundleFile = ConfigDefault.FormatBundleFile\n\t}\n\n\tif cfg.UnmarshalFunc == nil {\n\t\tcfg.UnmarshalFunc = ConfigDefault.UnmarshalFunc\n\t}\n\n\tif cfg.AcceptLanguages == nil {\n\t\tcfg.AcceptLanguages = append([]language.Tag(nil), ConfigDefault.AcceptLanguages...)\n\t}\n\n\tif cfg.Loader == nil {\n\t\tcfg.Loader = ConfigDefault.Loader\n\t}\n\n\tif cfg.LangHandler == nil {\n\t\tcfg.LangHandler = ConfigDefault.LangHandler\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/i18n/embed.go",
    "content": "//go:build go1.16\n\npackage i18n\n\nimport \"embed\"\n\ntype EmbedLoader struct {\n\tFS embed.FS\n}\n\nfunc (e *EmbedLoader) LoadMessage(path string) ([]byte, error) {\n\treturn e.FS.ReadFile(path)\n}\n"
  },
  {
    "path": "v3/i18n/embed_test.go",
    "content": "package i18n\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/nicksnyder/go-i18n/v2/i18n\"\n\t\"golang.org/x/text/language\"\n)\n\n//go:embed example/localizeJSON/*\nvar fs embed.FS\n\nfunc newEmbedServer() *fiber.App {\n\ttranslator := New(&Config{\n\t\tLoader:           &EmbedLoader{fs},\n\t\tUnmarshalFunc:    json.Unmarshal,\n\t\tRootPath:         \"./example/localizeJSON/\",\n\t\tFormatBundleFile: \"json\",\n\t})\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(translator.MustLocalize(ctx, \"welcome\"))\n\t})\n\tapp.Get(\"/:name\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(translator.MustLocalize(ctx, &i18n.LocalizeConfig{\n\t\t\tMessageID: \"welcomeWithName\",\n\t\t\tTemplateData: map[string]string{\n\t\t\t\t\"name\": ctx.Params(\"name\"),\n\t\t\t},\n\t\t}))\n\t})\n\treturn app\n}\n\nvar embedApp = newEmbedServer()\n\nfunc request(lang language.Tag, name string) (*http.Response, error) {\n\tpath := \"/\" + name\n\treq, _ := http.NewRequestWithContext(context.Background(), \"GET\", path, nil)\n\treq.Host = \"localhost\"\n\treq.Header.Add(\"Accept-Language\", lang.String())\n\treq.Method = \"GET\"\n\treq.RequestURI = path\n\tresp, err := embedApp.Test(req)\n\treturn resp, err\n}\n\nfunc TestEmbedLoader_LoadMessage(t *testing.T) {\n\tt.Parallel()\n\ttype args struct {\n\t\tlang language.Tag\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"hello world\",\n\t\t\targs: args{\n\t\t\t\tname: \"\",\n\t\t\t\tlang: language.English,\n\t\t\t},\n\t\t\twant: \"hello\",\n\t\t},\n\t\t{\n\t\t\tname: \"hello alex\",\n\t\t\targs: args{\n\t\t\t\tname: \"\",\n\t\t\t\tlang: language.Chinese,\n\t\t\t},\n\t\t\twant: \"你好\",\n\t\t},\n\t\t{\n\t\t\tname: \"hello alex\",\n\t\t\targs: args{\n\t\t\t\tname: \"alex\",\n\t\t\t\tlang: language.English,\n\t\t\t},\n\t\t\twant: \"hello alex\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := request(tt.args.lang, tt.args.name)\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tbody, err := io.ReadAll(got.Body)\n\t\t\tgot.Body.Close()\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tassert.Equal(t, tt.want, string(body))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "v3/i18n/example/localize/en.yaml",
    "content": "welcome: hello\nwelcomeWithName: hello {{ .name }}\n"
  },
  {
    "path": "v3/i18n/example/localize/zh.yaml",
    "content": "welcome: 你好\nwelcomeWithName: 你好 {{ .name }}\n"
  },
  {
    "path": "v3/i18n/example/localizeJSON/en.json",
    "content": "{\n  \"welcome\": \"hello\",\n  \"welcomeWithName\": \"hello {{ .name }}\"\n}\n"
  },
  {
    "path": "v3/i18n/example/localizeJSON/zh.json",
    "content": "{\n  \"welcome\": \"你好\",\n  \"welcomeWithName\": \"你好 {{ .name }}\"\n}\n"
  },
  {
    "path": "v3/i18n/example/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\n\tcontribi18n \"github.com/gofiber/contrib/v3/i18n\"\n\t\"github.com/gofiber/fiber/v3\"\n\tgoi18n \"github.com/nicksnyder/go-i18n/v2/i18n\"\n\t\"golang.org/x/text/language\"\n)\n\nfunc main() {\n\ttranslator := contribi18n.New(&contribi18n.Config{\n\t\tRootPath:        \"./localize\",\n\t\tAcceptLanguages: []language.Tag{language.Chinese, language.English},\n\t\tDefaultLanguage: language.Chinese,\n\t})\n\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tlocalize, err := translator.Localize(c, \"welcome\")\n\t\tif err != nil {\n\t\t\treturn c.Status(fiber.StatusInternalServerError).SendString(err.Error())\n\t\t}\n\t\treturn c.SendString(localize)\n\t})\n\tapp.Get(\"/:name\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(translator.MustLocalize(ctx, &goi18n.LocalizeConfig{\n\t\t\tMessageID: \"welcomeWithName\",\n\t\t\tTemplateData: map[string]string{\n\t\t\t\t\"name\": ctx.Params(\"name\"),\n\t\t\t},\n\t\t}))\n\t})\n\tlog.Fatal(app.Listen(\":3000\"))\n}\n"
  },
  {
    "path": "v3/i18n/go.mod",
    "content": "module github.com/gofiber/contrib/v3/i18n\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/nicksnyder/go-i18n/v2 v2.6.1\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/text v0.36.0\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/i18n/go.sum",
    "content": "github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=\ngithub.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\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": "v3/i18n/i18n.go",
    "content": "package i18n\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"sync\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/nicksnyder/go-i18n/v2/i18n\"\n\t\"golang.org/x/text/language\"\n)\n\n// I18n exposes thread-safe localization helpers backed by a shared bundle\n// and localizer map. Use New to construct an instance during application start\n// and reuse it across handlers.\ntype I18n struct {\n\tcfg *Config\n}\n\n// New prepares a thread-safe i18n container instance.\nfunc New(config ...*Config) *I18n {\n\tcfg := prepareConfig(config...)\n\n\treturn &I18n{cfg: cfg}\n}\n\nfunc prepareConfig(config ...*Config) *Config {\n\tsource := configDefault(config...)\n\n\tcfg := *source\n\n\tif source.AcceptLanguages != nil {\n\t\tcfg.AcceptLanguages = append([]language.Tag(nil), source.AcceptLanguages...)\n\t}\n\n\tbundle := i18n.NewBundle(cfg.DefaultLanguage)\n\tbundle.RegisterUnmarshalFunc(cfg.FormatBundleFile, cfg.UnmarshalFunc)\n\tcfg.bundle = bundle\n\n\tcfg.loadMessages()\n\tcfg.initLocalizerMap()\n\n\treturn &cfg\n}\n\nfunc (c *Config) loadMessage(filepath string) {\n\tbuf, err := c.Loader.LoadMessage(filepath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif _, err := c.bundle.ParseMessageFileBytes(buf, filepath); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (c *Config) loadMessages() *Config {\n\tfor _, lang := range c.AcceptLanguages {\n\t\tbundleFilePath := fmt.Sprintf(\"%s.%s\", lang.String(), c.FormatBundleFile)\n\t\tfilepath := path.Join(c.RootPath, bundleFilePath)\n\t\tc.loadMessage(filepath)\n\t}\n\treturn c\n}\n\nfunc (c *Config) initLocalizerMap() {\n\tlocalizerMap := &sync.Map{}\n\n\tfor _, lang := range c.AcceptLanguages {\n\t\ts := lang.String()\n\t\tlocalizerMap.Store(s, i18n.NewLocalizer(c.bundle, s))\n\t}\n\n\tlang := c.DefaultLanguage.String()\n\tif _, ok := localizerMap.Load(lang); !ok {\n\t\tlocalizerMap.Store(lang, i18n.NewLocalizer(c.bundle, lang))\n\t}\n\tc.localizerMap = localizerMap\n}\n\n/*\nMustLocalize get the i18n message without error handling\n\n\t  param is one of these type: messageID, *i18n.LocalizeConfig\n\t  Example:\n\t\tMustLocalize(ctx, \"hello\") // messageID is hello\n\t\tMustLocalize(ctx, &i18n.LocalizeConfig{\n\t\t\t\tMessageID: \"welcomeWithName\",\n\t\t\t\tTemplateData: map[string]string{\n\t\t\t\t\t\"name\": context.Param(\"name\"),\n\t\t\t\t},\n\t\t})\n*/\nfunc (i *I18n) MustLocalize(ctx fiber.Ctx, params interface{}) string {\n\tmessage, err := i.Localize(ctx, params)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn message\n}\n\n/*\nLocalize get the i18n message\n\n\t param is one of these type: messageID, *i18n.LocalizeConfig\n\t Example:\n\t\tLocalize(ctx, \"hello\") // messageID is hello\n\t\tLocalize(ctx, &i18n.LocalizeConfig{\n\t\t\t\tMessageID: \"welcomeWithName\",\n\t\t\t\tTemplateData: map[string]string{\n\t\t\t\t\t\"name\": context.Param(\"name\"),\n\t\t\t\t},\n\t\t})\n*/\nfunc (i *I18n) Localize(ctx fiber.Ctx, params interface{}) (string, error) {\n\tif i == nil || i.cfg == nil {\n\t\treturn \"\", fmt.Errorf(\"i18n.Localize error: %v\", \"translator is nil\")\n\t}\n\n\tappCfg := i.cfg\n\tlang := appCfg.LangHandler(ctx, appCfg.DefaultLanguage.String())\n\tlocalizer, _ := appCfg.localizerMap.Load(lang)\n\n\tif localizer == nil {\n\t\tdefaultLang := appCfg.DefaultLanguage.String()\n\t\tlocalizer, _ = appCfg.localizerMap.Load(defaultLang)\n\t}\n\n\tvar localizeConfig *i18n.LocalizeConfig\n\tswitch paramValue := params.(type) {\n\tcase string:\n\t\tlocalizeConfig = &i18n.LocalizeConfig{MessageID: paramValue}\n\tcase *i18n.LocalizeConfig:\n\t\tif paramValue == nil {\n\t\t\treturn \"\", fmt.Errorf(\"i18n.Localize error: %v\", \"params is nil\")\n\t\t}\n\t\tlocalizeConfig = paramValue\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"i18n.Localize error: %v\", \"unsupported params type\")\n\t}\n\n\tif localizer == nil {\n\t\treturn \"\", fmt.Errorf(\"i18n.Localize error: %v\", \"localizer is nil\")\n\t}\n\n\tloc, ok := localizer.(*i18n.Localizer)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"i18n.Localize error: %v\", \"unexpected localizer type\")\n\t}\n\tmessage, err := loc.Localize(localizeConfig)\n\tif err != nil {\n\t\tlog.Errorf(\"i18n.Localize error: %v\", err)\n\t\treturn \"\", fmt.Errorf(\"i18n.Localize error: %v\", err)\n\t}\n\treturn message, nil\n}\n"
  },
  {
    "path": "v3/i18n/i18n_test.go",
    "content": "package i18n\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/nicksnyder/go-i18n/v2/i18n\"\n\t\"golang.org/x/text/language\"\n)\n\nfunc newServer(translator *I18n) *fiber.App {\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(translator.MustLocalize(ctx, \"welcome\"))\n\t})\n\tapp.Get(\"/:name\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(translator.MustLocalize(ctx, &i18n.LocalizeConfig{\n\t\t\tMessageID: \"welcomeWithName\",\n\t\t\tTemplateData: map[string]string{\n\t\t\t\t\"name\": ctx.Params(\"name\"),\n\t\t\t},\n\t\t}))\n\t})\n\treturn app\n}\n\nvar (\n\tsharedTranslator = New()\n\ti18nApp          = newServer(sharedTranslator)\n)\n\nfunc makeRequest(lang language.Tag, name string, app *fiber.App) (*http.Response, error) {\n\tpath := \"/\" + name\n\treq, _ := http.NewRequestWithContext(context.Background(), \"GET\", path, nil)\n\treq.Host = \"localhost\"\n\tif lang != language.Und {\n\t\treq.Header.Add(\"Accept-Language\", lang.String())\n\t}\n\treq.Method = \"GET\"\n\treq.RequestURI = path\n\tresp, err := app.Test(req)\n\treturn resp, err\n}\n\nfunc TestI18nEN(t *testing.T) {\n\tt.Parallel()\n\ttype args struct {\n\t\tlang language.Tag\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"hello world\",\n\t\t\targs: args{\n\t\t\t\tname: \"\",\n\t\t\t\tlang: language.English,\n\t\t\t},\n\t\t\twant: \"hello\",\n\t\t},\n\t\t{\n\t\t\tname: \"hello alex\",\n\t\t\targs: args{\n\t\t\t\tname: \"alex\",\n\t\t\t\tlang: language.English,\n\t\t\t},\n\t\t\twant: \"hello alex\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := makeRequest(tt.args.lang, tt.args.name, i18nApp)\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tbody, err := io.ReadAll(got.Body)\n\t\t\tgot.Body.Close()\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tassert.Equal(t, tt.want, string(body))\n\t\t})\n\t}\n}\n\nfunc TestI18nZH(t *testing.T) {\n\ttype args struct {\n\t\tlang language.Tag\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"hello world\",\n\t\t\targs: args{\n\t\t\t\tname: \"\",\n\t\t\t\tlang: language.Chinese,\n\t\t\t},\n\t\t\twant: \"你好\",\n\t\t},\n\t\t{\n\t\t\tname: \"hello alex\",\n\t\t\targs: args{\n\t\t\t\tname: \"alex\",\n\t\t\t\tlang: language.Chinese,\n\t\t\t},\n\t\t\twant: \"你好 alex\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := makeRequest(tt.args.lang, tt.args.name, i18nApp)\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tbody, err := io.ReadAll(got.Body)\n\t\t\tgot.Body.Close()\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tassert.Equal(t, tt.want, string(body))\n\t\t})\n\t}\n}\n\nfunc TestParallelI18n(t *testing.T) {\n\ttype args struct {\n\t\tlang language.Tag\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"hello world\",\n\t\t\targs: args{\n\t\t\t\tname: \"\",\n\t\t\t\tlang: language.Chinese,\n\t\t\t},\n\t\t\twant: \"你好\",\n\t\t},\n\t\t{\n\t\t\tname: \"hello alex\",\n\t\t\targs: args{\n\t\t\t\tname: \"alex\",\n\t\t\t\tlang: language.Chinese,\n\t\t\t},\n\t\t\twant: \"你好 alex\",\n\t\t},\n\t\t{\n\t\t\tname: \"hello peter\",\n\t\t\targs: args{\n\t\t\t\tname: \"peter\",\n\t\t\t\tlang: language.English,\n\t\t\t},\n\t\t\twant: \"hello peter\",\n\t\t},\n\t}\n\tt.Parallel()\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := makeRequest(tt.args.lang, tt.args.name, i18nApp)\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tbody, err := io.ReadAll(got.Body)\n\t\t\tgot.Body.Close()\n\t\t\tassert.Equal(t, err, nil)\n\t\t\tassert.Equal(t, tt.want, string(body))\n\t\t})\n\t}\n}\n\nfunc TestTranslatorConcurrentLocalize(t *testing.T) {\n\tt.Parallel()\n\n\tconst workers = 64\n\tvar wg sync.WaitGroup\n\terrCh := make(chan error, workers)\n\n\tfor i := 0; i < workers; i++ {\n\t\tlang := language.English\n\t\tif i%2 == 1 {\n\t\t\tlang = language.Chinese\n\t\t}\n\t\tname := fmt.Sprintf(\"user-%d\", i)\n\t\twg.Add(1)\n\t\tgo func(lang language.Tag, name string) {\n\t\t\tdefer wg.Done()\n\t\t\tresp, err := makeRequest(lang, name, i18nApp)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\tresp.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\texpected := fmt.Sprintf(\"hello %s\", name)\n\t\t\tif lang == language.Chinese {\n\t\t\t\texpected = fmt.Sprintf(\"你好 %s\", name)\n\t\t\t}\n\t\t\tif string(body) != expected {\n\t\t\t\terrCh <- fmt.Errorf(\"unexpected body %q for lang %s\", string(body), lang.String())\n\t\t\t}\n\t\t}(lang, name)\n\t}\n\n\twg.Wait()\n\tclose(errCh)\n\n\tfor err := range errCh {\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestLocalize(t *testing.T) {\n\tt.Parallel()\n\ttranslator := New()\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tlocalize, err := translator.Localize(ctx, \"welcome?\")\n\t\tassert.Equal(t, \"\", localize)\n\t\treturn fiber.NewError(500, err.Error())\n\t})\n\n\tapp.Get(\"/:name\", func(ctx fiber.Ctx) error {\n\t\tname := ctx.Params(\"name\")\n\t\tlocalize, err := translator.Localize(ctx, &i18n.LocalizeConfig{\n\t\t\tMessageID: \"welcomeWithName\",\n\t\t\tTemplateData: map[string]string{\n\t\t\t\t\"name\": name,\n\t\t\t},\n\t\t})\n\t\tassert.Equal(t, nil, err)\n\t\treturn ctx.SendString(localize)\n\t})\n\n\tt.Run(\"test localize\", func(t *testing.T) {\n\t\tgot, err := makeRequest(language.Chinese, \"\", app)\n\t\tassert.Equal(t, 500, got.StatusCode)\n\t\tassert.Equal(t, nil, err)\n\t\tbody, _ := io.ReadAll(got.Body)\n\t\tgot.Body.Close()\n\t\tassert.Equal(t, `i18n.Localize error: message \"welcome?\" not found in language \"zh\"`, string(body))\n\n\t\tgot, err = makeRequest(language.English, \"name\", app)\n\t\tassert.Equal(t, 200, got.StatusCode)\n\t\tassert.Equal(t, nil, err)\n\t\tbody, _ = io.ReadAll(got.Body)\n\t\tgot.Body.Close()\n\t\tassert.Equal(t, \"hello name\", string(body))\n\t})\n}\n\nfunc TestNew_doesNotMutateCallerConfig(t *testing.T) {\n\tt.Parallel()\n\tcfg := &Config{\n\t\tRootPath:        \"./example/localize\",\n\t\tAcceptLanguages: []language.Tag{language.Chinese, language.English},\n\t}\n\t// DefaultLanguage is intentionally left as zero value (language.Und)\n\t// to verify that New() does not mutate the caller's config.\n\toriginalDefaultLanguage := cfg.DefaultLanguage\n\n\ttranslator := New(cfg)\n\n\tassert.NotNil(t, translator)\n\tassert.Equal(t, originalDefaultLanguage, cfg.DefaultLanguage, \"New() must not mutate the caller's Config\")\n\tassert.Equal(t, language.Und, cfg.DefaultLanguage, \"caller's DefaultLanguage should remain Und\")\n}\n\nfunc TestLocalize_nilReceiver(t *testing.T) {\n\tt.Parallel()\n\tvar translator *I18n\n\tlocalize, err := translator.Localize(nil, \"welcome\")\n\tassert.Equal(t, \"\", localize)\n\tassert.EqualError(t, err, \"i18n.Localize error: translator is nil\")\n}\n\nfunc TestLocalize_unsupportedParamsType(t *testing.T) {\n\tt.Parallel()\n\ttranslator := New()\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tlocalize, err := translator.Localize(ctx, 42)\n\t\tassert.Equal(t, \"\", localize)\n\t\treturn fiber.NewError(500, err.Error())\n\t})\n\tgot, err := makeRequest(language.English, \"\", app)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 500, got.StatusCode)\n\tbody, _ := io.ReadAll(got.Body)\n\tgot.Body.Close()\n\tassert.Equal(t, \"i18n.Localize error: unsupported params type\", string(body))\n}\n\nfunc TestLocalize_nilLocalizeConfig(t *testing.T) {\n\tt.Parallel()\n\ttranslator := New()\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tlocalize, err := translator.Localize(ctx, (*i18n.LocalizeConfig)(nil))\n\t\tassert.Equal(t, \"\", localize)\n\t\treturn fiber.NewError(500, err.Error())\n\t})\n\tgot, err := makeRequest(language.English, \"\", app)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 500, got.StatusCode)\n\tbody, _ := io.ReadAll(got.Body)\n\tgot.Body.Close()\n\tassert.Equal(t, \"i18n.Localize error: params is nil\", string(body))\n}\n\nfunc TestMustLocalize_panics(t *testing.T) {\n\tt.Parallel()\n\ttranslator := New()\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tassert.Panics(t, func() {\n\t\t\ttranslator.MustLocalize(ctx, \"nonexistent_message\")\n\t\t})\n\t\treturn nil\n\t})\n\t_, err := makeRequest(language.English, \"\", app)\n\tassert.NoError(t, err)\n}\n\nfunc Test_defaultLangHandler(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(defaultLangHandler(nil, language.English.String()))\n\t})\n\tapp.Get(\"/test\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(defaultLangHandler(c, language.English.String()))\n\t})\n\tt.Parallel()\n\tt.Run(\"test nil ctx\", func(t *testing.T) {\n\t\tvar wg sync.WaitGroup\n\t\twant := 100\n\t\twg.Add(want)\n\t\tfor i := 0; i < want; i++ {\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tgot, err := makeRequest(language.English, \"\", app)\n\t\t\t\tassert.Equal(t, nil, err)\n\t\t\t\tbody, _ := io.ReadAll(got.Body)\n\t\t\t\tgot.Body.Close()\n\t\t\t\tassert.Equal(t, \"en\", string(body))\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t})\n\n\tt.Run(\"test query and header\", func(t *testing.T) {\n\t\tgot, err := makeRequest(language.Chinese, \"test?lang=en\", app)\n\t\tassert.Equal(t, nil, err)\n\t\tbody, _ := io.ReadAll(got.Body)\n\t\tgot.Body.Close()\n\t\tassert.Equal(t, \"en\", string(body))\n\n\t\tgot, err = makeRequest(language.Chinese, \"test\", app)\n\t\tassert.Equal(t, nil, err)\n\t\tbody, _ = io.ReadAll(got.Body)\n\t\tgot.Body.Close()\n\t\tassert.Equal(t, \"zh\", string(body))\n\n\t\tgot, err = makeRequest(language.Chinese, \"test\", app)\n\t\tassert.Equal(t, nil, err)\n\t\tbody, _ = io.ReadAll(got.Body)\n\t\tgot.Body.Close()\n\t\tassert.Equal(t, \"zh\", string(body))\n\t})\n}\n"
  },
  {
    "path": "v3/jwt/README.md",
    "content": "---\nid: jwt\n---\n\n# JWT\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*jwt*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20jwt/badge.svg)\n\nJWT returns a JSON Web Token (JWT) auth middleware.\nFor valid token, it sets the token in Ctx.Locals (and in the underlying `context.Context` when `PassLocalsToContext` is enabled) and calls next handler.\nFor invalid token, it returns \"401 - Unauthorized\" error.\nFor missing token, it returns \"400 - Bad Request\" error.\n\nSpecial thanks and credits to [Echo](https://echo.labstack.com/middleware/jwt)\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```bash\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/jwt\ngo get -u github.com/golang-jwt/jwt/v5\n```\n\n## Signature\n\n```go\njwtware.New(config ...jwtware.Config) func(fiber.Ctx) error\njwtware.FromContext(ctx any) *jwt.Token    // jwt \"github.com/golang-jwt/jwt/v5\"\n```\n\n`FromContext` accepts a `fiber.Ctx`, `fiber.CustomCtx`, `*fasthttp.RequestCtx`, or a standard `context.Context` (e.g. the value returned by `c.Context()` when `PassLocalsToContext` is enabled). It returns a `*jwt.Token` from `github.com/golang-jwt/jwt/v5`.\n\n## Config\n\n| Property           | Type                                 | Description                                                                                                  | Default                      |\n|:-------------------|:-------------------------------------|:-------------------------------------------------------------------------------------------------------------|:-----------------------------|\n| Next               | `func(fiber.Ctx) bool`               | Defines a function to skip this middleware when it returns true                                              | `nil`                        |\n| SuccessHandler     | `func(fiber.Ctx) error`              | Executed when a token is valid.                                                                               | `c.Next()`                   |\n| ErrorHandler       | `func(fiber.Ctx, error) error`       | ErrorHandler defines a function which is executed for an invalid token.                                      | `401 Invalid or expired JWT` |\n| SigningKey         | `SigningKey`                         | Signing key used to validate the token. Used as a fallback if `SigningKeys` is empty.                        | `nil`                        |\n| SigningKeys        | `map[string]SigningKey`              | Map of signing keys used to validate tokens via the `kid` header.                                            | `nil`                        |\n| Claims             | `jwt.Claims`                         | Claims are extendable claims data defining token content.                                                    | `jwt.MapClaims{}`            |\n| Extractor          | `Extractor`                          | Function used to extract the token from the request.                                                         | `FromAuthHeader(\"Bearer\")`   |\n| TokenProcessorFunc | `func(token string) (string, error)` | TokenProcessorFunc processes the token extracted using the Extractor.                                        | `nil`                        |\n| KeyFunc            | `jwt.Keyfunc`                        | User-defined function that supplies the public key for token validation.                                     | `nil` (uses internal default)|\n| JWKSetURLs         | `[]string`                           | List of JSON Web Key (JWK) Set URLs used to obtain signing keys for parsing JWTs.                            | `nil`                        |\n\n## Available Extractors\n\nJWT middleware uses the shared Fiber extractors (github.com/gofiber/fiber/v3/extractors) and provides several helpers for different token sources. Import them with:\n\n```go\nimport \"github.com/gofiber/fiber/v3/extractors\"\n```\n\nFor an overview and additional examples, see the Fiber Extractors guide:\n\n- https://docs.gofiber.io/guide/extractors\n\n- `extractors.FromAuthHeader(prefix string)` - Extracts token from the Authorization header using the given scheme prefix (e.g., \"Bearer\"). **This is the recommended and most secure method.**\n- `extractors.FromHeader(header string)` - Extracts token from the specified HTTP header\n- `extractors.FromQuery(param string)` - Extracts token from URL query parameters\n- `extractors.FromParam(param string)` - Extracts token from URL path parameters\n- `extractors.FromCookie(key string)` - Extracts token from cookies\n- `extractors.FromForm(param string)` - Extracts token from form data\n- `extractors.Chain(extrs ...extractors.Extractor)` - Tries multiple extractors in order until one succeeds\n\n### Security Considerations\n\n⚠️ **Security Warning**: When choosing an extractor, consider the security implications:\n\n- **URL-based extractors** (`FromQuery`, `FromParam`): Tokens can leak through server logs, browser referrer headers, proxy logs, and browser history. Use only for development or when security is not a primary concern.\n- **Form-based extractors** (`FromForm`): Similar risks to URL extractors, especially if forms are submitted via GET requests.\n- **Header-based extractors** (`FromAuthHeader`, `FromHeader`): Most secure as headers are not typically logged or exposed in referrers.\n- **Cookie-based extractors** (`FromCookie`): Secure for web applications but requires proper cookie security settings (HttpOnly, Secure, SameSite).\n\n**Recommendation**: Use `FromAuthHeader(\"Bearer\")` (the default) for production applications unless you have specific requirements that necessitate alternative extractors.\n\n## HS256 Example\n\n```go\npackage main\n\nimport (\n \"time\"\n\n \"github.com/gofiber/fiber/v3\"\n \"github.com/gofiber/fiber/v3/extractors\"\n\n jwtware \"github.com/gofiber/contrib/v3/jwt\"\n \"github.com/golang-jwt/jwt/v5\"\n)\n\nfunc main() {\n app := fiber.New()\n\n // Login route\n app.Post(\"/login\", login)\n\n // Unauthenticated route\n app.Get(\"/\", accessible)\n\n // JWT Middleware\n app.Use(jwtware.New(jwtware.Config{\n  SigningKey: jwtware.SigningKey{Key: []byte(\"secret\")},\n  Extractor:  extractors.FromAuthHeader(\"Bearer\"),\n }))\n\n // Restricted Routes\n app.Get(\"/restricted\", restricted)\n\n app.Listen(\":3000\")\n}\n\nfunc login(c fiber.Ctx) error {\n user := c.FormValue(\"user\")\n pass := c.FormValue(\"pass\")\n\n // Throws Unauthorized error\n if user != \"john\" || pass != \"doe\" {\n  return c.SendStatus(fiber.StatusUnauthorized)\n }\n\n // Create the Claims\n claims := jwt.MapClaims{\n  \"name\":  \"John Doe\",\n  \"admin\": true,\n  \"exp\":   time.Now().Add(time.Hour * 72).Unix(),\n }\n\n // Create token\n token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\n // Generate encoded token and send it as response.\n t, err := token.SignedString([]byte(\"secret\"))\n if err != nil {\n  return c.SendStatus(fiber.StatusInternalServerError)\n }\n\n return c.JSON(fiber.Map{\"token\": t})\n}\n\nfunc accessible(c fiber.Ctx) error {\n return c.SendString(\"Accessible\")\n}\n\nfunc restricted(c fiber.Ctx) error {\n    user := jwtware.FromContext(c)\n    claims := user.Claims.(jwt.MapClaims)\n    name := claims[\"name\"].(string)\n    return c.SendString(\"Welcome \" + name)\n}\n\n```\n\n## Cookie Extractor Example\n\n```go\npackage main\n\nimport (\n \"github.com/gofiber/fiber/v3\"\n\n jwtware \"github.com/gofiber/contrib/v3/jwt\"\n)\n\nfunc main() {\n app := fiber.New()\n\n // JWT Middleware with cookie extractor\n app.Use(jwtware.New(jwtware.Config{\n  SigningKey: jwtware.SigningKey{Key: []byte(\"secret\")},\n  Extractor:  extractors.FromCookie(\"token\"),\n }))\n\n app.Get(\"/protected\", func(c fiber.Ctx) error {\n  return c.SendString(\"Protected route\")\n })\n\n app.Listen(\":3000\")\n}\n```\n\n## HS256 Test\n\n_Login using username and password to retrieve a token._\n\n```bash\ncurl --data \"user=john&pass=doe\" http://localhost:3000/login\n```\n\n_Response_\n\n```json\n{\n  \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY\"\n}\n```\n\n_Request a restricted resource using the token in Authorization request header._\n\n```bash\ncurl localhost:3000/restricted -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY\"\n```\n\n_Response_\n\n```text\nWelcome John Doe\n```\n\n## RS256 Example\n\n```go\npackage main\n\nimport (\n \"crypto/rand\"\n \"crypto/rsa\"\n \"log\"\n \"time\"\n\n \"github.com/gofiber/fiber/v3\"\n\n \"github.com/golang-jwt/jwt/v5\"\n\n jwtware \"github.com/gofiber/contrib/v3/jwt\"\n)\n\nvar (\n // Obviously, this is just a test example. Do not do this in production.\n // In production, you would have the private key and public key pair generated\n // in advance. NEVER add a private key to any GitHub repo.\n privateKey *rsa.PrivateKey\n)\n\nfunc main() {\n app := fiber.New()\n\n // Just as a demo, generate a new private/public key pair on each run. See note above.\n rng := rand.Reader\n var err error\n privateKey, err = rsa.GenerateKey(rng, 2048)\n if err != nil {\n  log.Fatalf(\"rsa.GenerateKey: %v\", err)\n }\n\n // Login route\n app.Post(\"/login\", login)\n\n // Unauthenticated route\n app.Get(\"/\", accessible)\n\n // JWT Middleware\n app.Use(jwtware.New(jwtware.Config{\n  SigningKey: jwtware.SigningKey{\n   JWTAlg: jwtware.RS256,\n   Key:    privateKey.Public(),\n  },\n  Extractor: extractors.FromAuthHeader(\"Bearer\"),\n }))\n\n // Restricted Routes\n app.Get(\"/restricted\", restricted)\n\n app.Listen(\":3000\")\n}\n\nfunc login(c fiber.Ctx) error {\n user := c.FormValue(\"user\")\n pass := c.FormValue(\"pass\")\n\n // Throws Unauthorized error\n if user != \"john\" || pass != \"doe\" {\n  return c.SendStatus(fiber.StatusUnauthorized)\n }\n\n // Create the Claims\n claims := jwt.MapClaims{\n  \"name\":  \"John Doe\",\n  \"admin\": true,\n  \"exp\":   time.Now().Add(time.Hour * 72).Unix(),\n }\n\n // Create token\n token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\n // Generate encoded token and send it as response.\n t, err := token.SignedString(privateKey)\n if err != nil {\n  log.Printf(\"token.SignedString: %v\", err)\n  return c.SendStatus(fiber.StatusInternalServerError)\n }\n\n return c.JSON(fiber.Map{\"token\": t})\n}\n\nfunc accessible(c fiber.Ctx) error {\n return c.SendString(\"Accessible\")\n}\n\nfunc restricted(c fiber.Ctx) error {\n    user := jwtware.FromContext(c)\n    claims := user.Claims.(jwt.MapClaims)\n    name := claims[\"name\"].(string)\n    return c.SendString(\"Welcome \" + name)\n}\n```\n\n## Retrieving the token with PassLocalsToContext\n\nWhen `fiber.Config{PassLocalsToContext: true}` is set, the JWT token stored by the middleware is also available in the underlying `context.Context`. Use `jwtware.FromContext` with any of the supported context types:\n\n```go\n// From a fiber.Ctx (most common usage)\ntoken := jwtware.FromContext(c)\n\n// From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled)\ntoken := jwtware.FromContext(c.Context())\n```\n\n## RS256 Test\n\nThe RS256 is actually identical to the HS256 test above.\n\n## JWK Set Test\n\nThe tests are identical to basic `JWT` tests above, with exception that `JWKSetURLs` to valid public keys collection in JSON Web Key (JWK) Set format should be supplied. See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517).\n\n## Custom KeyFunc example\n\nKeyFunc defines a user-defined function that supplies the public key for a token validation.\nThe function shall take care of verifying the signing algorithm and selecting the proper key.\nA user-defined KeyFunc can be useful if tokens are issued by an external party.\n\nWhen a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.\nThis is one of the three options to provide a token validation key.\nThe order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.\nRequired if neither SigningKeys nor SigningKey is provided.\nDefault to an internal implementation verifying the signing algorithm and selecting the proper key.\n\n```go\npackage main\n\nimport (\n \"fmt\"\n  \"github.com/gofiber/fiber/v3\"\n\n  jwtware \"github.com/gofiber/contrib/v3/jwt\"\n  \"github.com/golang-jwt/jwt/v5\"\n)\n\nfunc main() {\n app := fiber.New()\n\n app.Use(jwtware.New(jwtware.Config{\n  KeyFunc:   customKeyFunc(),\n  Extractor: extractors.FromAuthHeader(\"Bearer\"),\n }))\n\n app.Get(\"/ok\", func(c fiber.Ctx) error {\n  return c.SendString(\"OK\")\n })\n}\n\nfunc customKeyFunc() jwt.Keyfunc {\n return func(t *jwt.Token) (interface{}, error) {\n  // Always check the signing method\n  if t.Method.Alg() != jwtware.HS256 {\n   return nil, fmt.Errorf(\"Unexpected jwt signing method=%v\", t.Header[\"alg\"])\n  }\n\n  // TODO custom implementation of loading signing key like from a database\n    signingKey := \"secret\"\n\n  return []byte(signingKey), nil\n }\n}\n```\n"
  },
  {
    "path": "v3/jwt/config.go",
    "content": "package jwtware\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/MicahParks/keyfunc/v2\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\nvar (\n\t// ErrJWTAlg is returned when the JWT header did not contain the expected algorithm.\n\tErrJWTAlg = errors.New(\"the JWT header did not contain the expected algorithm\")\n\n\t// ErrMissingToken is returned when no JWT token is found in the request.\n\tErrMissingToken = errors.New(\"missing or malformed JWT\")\n)\n\n// Config defines the config for JWT 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(fiber.Ctx) bool\n\n\t// SuccessHandler is executed when a token is successfully validated.\n\t// Optional. Default: nil\n\tSuccessHandler fiber.Handler\n\n\t// ErrorHandler is executed when token validation fails.\n\t// It allows customization of JWT error responses.\n\t// Optional. Default: 401 Invalid or expired JWT\n\tErrorHandler fiber.ErrorHandler\n\n\t// SigningKey is the primary key used to validate tokens.\n\t// Used as a fallback if SigningKeys is empty.\n\t// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.\n\tSigningKey SigningKey\n\n\t// SigningKeys is a map of keys used to validate tokens with the \"kid\" field.\n\t// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.\n\tSigningKeys map[string]SigningKey\n\n\t// Claims are extendable claims data defining token content.\n\t// Optional. Default value jwt.MapClaims\n\tClaims jwt.Claims\n\n\t// Extractor defines a function to extract the token from the request.\n\t// Optional. Default: FromAuthHeader(\"Bearer\").\n\tExtractor extractors.Extractor\n\n\t// TokenProcessorFunc processes the token extracted using the Extractor.\n\t// Optional. Default: nil\n\tTokenProcessorFunc func(token string) (string, error)\n\n\t// KeyFunc provides the public key for JWT verification.\n\t// It handles algorithm verification and key selection.\n\t// By default, the github.com/MicahParks/keyfunc/v2 package is used.\n\t// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.\n\tKeyFunc jwt.Keyfunc\n\n\t// JWKSetURLs is a list of URLs containing JSON Web Key Sets (JWKS) for signature verification.\n\t// HTTPS is recommended. The \"kid\" field in the JWT header and JWKs is mandatory.\n\t// Default behavior:\n\t// - Refresh every hour.\n\t// - Auto-refresh on new \"kid\" in JWT.\n\t// - Rate limit refreshes to once every 5 minutes.\n\t// - Timeout refreshes after 10 seconds.\n\t// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.\n\tJWKSetURLs []string\n}\n\n// SigningKey holds information about the recognized cryptographic keys used to sign JWTs by this program.\ntype SigningKey struct {\n\t// JWTAlg is the algorithm used to sign JWTs. If this value is a non-empty string, this will be checked against the\n\t// \"alg\" value in the JWT header.\n\t//\n\t// https://www.rfc-editor.org/rfc/rfc7518#section-3.1\n\tJWTAlg string\n\t// Key is the cryptographic key used to sign JWTs. For supported types, please see\n\t// https://github.com/golang-jwt/jwt.\n\tKey interface{}\n}\n\n// makeCfg function will check correctness of supplied configuration\n// and will complement it with default values instead of missing ones\nfunc makeCfg(config []Config) (cfg Config) {\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\t}\n\tif cfg.SuccessHandler == nil {\n\t\tcfg.SuccessHandler = func(c fiber.Ctx) error {\n\t\t\treturn c.Next()\n\t\t}\n\t}\n\tif cfg.ErrorHandler == nil {\n\t\tcfg.ErrorHandler = func(c fiber.Ctx, err error) error {\n\t\t\tif errors.Is(err, extractors.ErrNotFound) {\n\t\t\t\treturn c.Status(fiber.StatusBadRequest).SendString(ErrMissingToken.Error())\n\t\t\t}\n\t\t\tif e, ok := err.(*fiber.Error); ok {\n\t\t\t\treturn c.Status(e.Code).SendString(e.Message)\n\t\t\t}\n\t\t\treturn c.Status(fiber.StatusUnauthorized).SendString(\"Invalid or expired JWT\")\n\t\t}\n\t}\n\tif cfg.SigningKey.Key == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil {\n\t\tpanic(\"Fiber: JWT middleware configuration: At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.\")\n\t}\n\tif len(cfg.SigningKeys) > 0 {\n\t\tfor _, key := range cfg.SigningKeys {\n\t\t\tif key.Key == nil {\n\t\t\t\tpanic(\"Fiber: JWT middleware configuration: SigningKey.Key cannot be nil\")\n\t\t\t}\n\t\t}\n\t}\n\tif len(cfg.JWKSetURLs) > 0 {\n\t\tfor _, u := range cfg.JWKSetURLs {\n\t\t\tparsed, err := url.Parse(u)\n\t\t\tif err != nil || parsed.Scheme == \"\" || parsed.Host == \"\" {\n\t\t\t\tpanic(\"Fiber: JWT middleware configuration: Invalid JWK Set URL (must be absolute http/https): \" + u)\n\t\t\t}\n\t\t\tif parsed.Scheme != \"https\" && parsed.Scheme != \"http\" {\n\t\t\t\tpanic(\"Fiber: JWT middleware configuration: Unsupported JWK Set URL scheme: \" + parsed.Scheme)\n\t\t\t}\n\t\t}\n\t}\n\tif cfg.Claims == nil {\n\t\tcfg.Claims = jwt.MapClaims{}\n\t}\n\tif cfg.Extractor.Extract == nil {\n\t\tcfg.Extractor = extractors.FromAuthHeader(\"Bearer\")\n\t}\n\n\tif cfg.KeyFunc == nil {\n\t\tif len(cfg.SigningKeys) > 0 || len(cfg.JWKSetURLs) > 0 {\n\t\t\tvar givenKeys map[string]keyfunc.GivenKey\n\t\t\tif cfg.SigningKeys != nil {\n\t\t\t\tgivenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys))\n\t\t\t\tfor kid, key := range cfg.SigningKeys {\n\t\t\t\t\tgivenKeys[kid] = keyfunc.NewGivenCustom(key.Key, keyfunc.GivenKeyOptions{\n\t\t\t\t\t\tAlgorithm: key.JWTAlg,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(cfg.JWKSetURLs) > 0 {\n\t\t\t\tvar err error\n\t\t\t\tcfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(\"Failed to create keyfunc from JWK Set URL: \" + err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc\n\t\t\t}\n\t\t} else {\n\t\t\tcfg.KeyFunc = signingKeyFunc(cfg.SigningKey)\n\t\t}\n\t}\n\n\treturn cfg\n}\n\nfunc multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) {\n\topts := keyfuncOptions(givenKeys)\n\tmultiple := make(map[string]keyfunc.Options, len(jwkSetURLs))\n\tfor _, url := range jwkSetURLs {\n\t\tmultiple[url] = opts\n\t}\n\tmultiOpts := keyfunc.MultipleOptions{\n\t\tKeySelector: keyfunc.KeySelectorFirst,\n\t}\n\tmulti, err := keyfunc.GetMultiple(multiple, multiOpts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get multiple JWK Set URLs: %w\", err)\n\t}\n\treturn multi.Keyfunc, nil\n}\n\nfunc keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options {\n\treturn keyfunc.Options{\n\t\tGivenKeys: givenKeys,\n\t\tRefreshErrorHandler: func(err error) {\n\t\t\tlog.Printf(\"Failed to perform background refresh of JWK Set: %s.\", err)\n\t\t},\n\t\tRefreshInterval:   time.Hour,\n\t\tRefreshRateLimit:  time.Minute * 5,\n\t\tRefreshTimeout:    time.Second * 10,\n\t\tRefreshUnknownKID: true,\n\t}\n}\n\nfunc signingKeyFunc(key SigningKey) jwt.Keyfunc {\n\treturn func(token *jwt.Token) (interface{}, error) {\n\t\tif key.JWTAlg != \"\" {\n\t\t\talg, ok := token.Header[\"alg\"].(string)\n\t\t\tif !ok {\n\t\t\t\treturn nil, fmt.Errorf(\"unexpected jwt signing method: expected: %q: got: missing or unexpected JSON type\", key.JWTAlg)\n\t\t\t}\n\t\t\tif alg != key.JWTAlg {\n\t\t\t\treturn nil, fmt.Errorf(\"unexpected jwt signing method: expected: %q: got: %q\", key.JWTAlg, alg)\n\t\t\t}\n\t\t}\n\t\treturn key.Key, nil\n\t}\n}\n"
  },
  {
    "path": "v3/jwt/config_test.go",
    "content": "package jwtware\n\nimport (\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/golang-jwt/jwt/v5\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPanicOnMissingConfiguration(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err == nil {\n\t\t\tt.Fatalf(\"Middleware should panic on missing configuration\")\n\t\t}\n\t}()\n\n\t// Arrange\n\tconfig := make([]Config, 0)\n\n\t// Act\n\tmakeCfg(config)\n}\n\nfunc TestDefaultConfiguration(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tconfig := append(make([]Config, 0), Config{\n\t\tSigningKey: SigningKey{Key: []byte(\"\")},\n\t})\n\n\t// Act\n\tcfg := makeCfg(config)\n\n\t// Assert\n\trequire.NotNil(t, cfg.Claims, \"Default claims should not be 'nil'\")\n\trequire.Equal(t, extractors.SourceAuthHeader, cfg.Extractor.Source, \"Default extractor source should be '%v'\", extractors.SourceAuthHeader)\n\trequire.Equal(t, fiber.HeaderAuthorization, cfg.Extractor.Key, \"Default extractor key should be '%v'\", fiber.HeaderAuthorization)\n\trequire.Equal(t, \"Bearer\", cfg.Extractor.AuthScheme, \"Default auth scheme should be 'Bearer'\")\n}\n\nfunc TestCustomExtractor(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\textractor := extractors.FromHeader(\"X-Auth-Token\")\n\tconfig := append(make([]Config, 0), Config{\n\t\tSigningKey: SigningKey{Key: []byte(\"\")},\n\t\tExtractor:  extractor,\n\t})\n\n\t// Act\n\tcfg := makeCfg(config)\n\n\t// Assert\n\trequire.Equal(t, extractor.Source, cfg.Extractor.Source, \"Extractor source should be the custom one\")\n\trequire.Equal(t, extractor.Key, cfg.Extractor.Key, \"Extractor key should be the custom one\")\n\trequire.Equal(t, \"\", cfg.Extractor.AuthScheme, \"AuthScheme should be empty for non-Authorization extractors\")\n}\n\nfunc TestPanicOnInvalidSigningKey(t *testing.T) {\n\tt.Parallel()\n\tconfig := append(make([]Config, 0), Config{\n\t\tSigningKey: SigningKey{Key: nil}, // Invalid key\n\t})\n\trequire.Panics(t, func() { makeCfg(config) })\n}\n\nfunc TestPanicOnInvalidSigningKeys(t *testing.T) {\n\tt.Parallel()\n\tconfig := append(make([]Config, 0), Config{\n\t\tSigningKeys: map[string]SigningKey{\n\t\t\t\"key1\": {Key: nil}, // Invalid key\n\t\t},\n\t})\n\trequire.Panics(t, func() { makeCfg(config) })\n}\n\nfunc TestPanicOnInvalidJWKSetURLs(t *testing.T) {\n\tt.Parallel()\n\t// Arrange\n\tconfig := append(make([]Config, 0), Config{\n\t\tJWKSetURLs: []string{\"invalid-url\"}, // This would cause panic in keyfunc\n\t})\n\trequire.Panics(t, func() { makeCfg(config) })\n}\n\nfunc TestCustomClaims(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tcustomClaims := jwt.MapClaims{\"custom\": \"claims\"}\n\tconfig := append(make([]Config, 0), Config{\n\t\tSigningKey: SigningKey{Key: []byte(\"\")},\n\t\tClaims:     customClaims,\n\t})\n\n\t// Act\n\tcfg := makeCfg(config)\n\n\t// Assert\n\trequire.NotNil(t, cfg.Claims, \"Custom claims should be preserved\")\n\n\t// Check if it's the same map by checking a key\n\tclaimsMap, ok := cfg.Claims.(jwt.MapClaims)\n\trequire.True(t, ok, \"Claims should be MapClaims\")\n\trequire.Equal(t, \"claims\", claimsMap[\"custom\"], \"Custom claims content should be preserved\")\n}\n\nfunc TestTokenProcessorFunc_Configured(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tconfig := append(make([]Config, 0), Config{\n\t\tSigningKey: SigningKey{Key: []byte(\"\")},\n\t\tTokenProcessorFunc: func(token string) (string, error) {\n\t\t\treturn \"\", fmt.Errorf(\"processing failed\")\n\t\t},\n\t})\n\n\t// Act\n\tcfg := makeCfg(config)\n\n\t// Assert\n\trequire.NotNil(t, cfg.TokenProcessorFunc, \"TokenProcessorFunc should be set\")\n\n\t// Exercise the processor\n\t_, err := cfg.TokenProcessorFunc(\"dummy\")\n\trequire.Error(t, err, \"TokenProcessorFunc should return error\")\n}\n\nfunc TestPanicOnUnsupportedJWKSetURLScheme(t *testing.T) {\n\tt.Parallel()\n\tconfig := append(make([]Config, 0), Config{\n\t\tJWKSetURLs: []string{\"ftp://example.com\"}, // Unsupported scheme\n\t})\n\trequire.Panics(t, func() { makeCfg(config) })\n}\n"
  },
  {
    "path": "v3/jwt/crypto.go",
    "content": "package jwtware\n\nconst (\n\t// HS256 represents a public cryptography key generated by a 256 bit HMAC algorithm.\n\tHS256 = \"HS256\"\n\n\t// HS384 represents a public cryptography key generated by a 384 bit HMAC algorithm.\n\tHS384 = \"HS384\"\n\n\t// HS512 represents a public cryptography key generated by a 512 bit HMAC algorithm.\n\tHS512 = \"HS512\"\n\n\t// ES256 represents a public cryptography key generated by a 256 bit ECDSA algorithm.\n\tES256 = \"ES256\"\n\n\t// ES384 represents a public cryptography key generated by a 384 bit ECDSA algorithm.\n\tES384 = \"ES384\"\n\n\t// ES512 represents a public cryptography key generated by a 512 bit ECDSA algorithm.\n\tES512 = \"ES512\"\n\n\t// P256 represents a cryptographic elliptical curve type.\n\tP256 = \"P-256\"\n\n\t// P384 represents a cryptographic elliptical curve type.\n\tP384 = \"P-384\"\n\n\t// P521 represents a cryptographic elliptical curve type.\n\tP521 = \"P-521\"\n\n\t// RS256 represents a public cryptography key generated by a 256 bit RSA algorithm.\n\tRS256 = \"RS256\"\n\n\t// RS384 represents a public cryptography key generated by a 384 bit RSA algorithm.\n\tRS384 = \"RS384\"\n\n\t// RS512 represents a public cryptography key generated by a 512 bit RSA algorithm.\n\tRS512 = \"RS512\"\n\n\t// PS256 represents a public cryptography key generated by a 256 bit RSA algorithm.\n\tPS256 = \"PS256\"\n\n\t// PS384 represents a public cryptography key generated by a 384 bit RSA algorithm.\n\tPS384 = \"PS384\"\n\n\t// PS512 represents a public cryptography key generated by a 512 bit RSA algorithm.\n\tPS512 = \"PS512\"\n)\n"
  },
  {
    "path": "v3/jwt/go.mod",
    "content": "module github.com/gofiber/contrib/v3/jwt\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/MicahParks/keyfunc/v2 v2.1.0\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/jwt/go.sum",
    "content": "github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=\ngithub.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=\ngithub.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/jwt/jwt.go",
    "content": "// 🚀 Fiber is an Express inspired web framework written in Go with 💖\n// 📌 API Documentation: https://fiber.wiki\n// 📝 Github Repository: https://github.com/gofiber/fiber\n// Special thanks to Echo: https://github.com/labstack/echo/blob/master/middleware/jwt.go\n\npackage jwtware\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/golang-jwt/jwt/v5\"\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 following contextKey values are defined to store values in context.\nconst (\n\ttokenKey contextKey = iota\n)\n\n// New ...\nfunc New(config ...Config) fiber.Handler {\n\tcfg := makeCfg(config)\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\t\tauth, err := cfg.Extractor.Extract(c)\n\t\tif err != nil {\n\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t}\n\n\t\tif cfg.TokenProcessorFunc != nil {\n\t\t\tauth, err = cfg.TokenProcessorFunc(auth)\n\t\t\tif err != nil {\n\t\t\t\treturn cfg.ErrorHandler(c, err)\n\t\t\t}\n\t\t}\n\n\t\tvar token *jwt.Token\n\t\tif _, ok := cfg.Claims.(jwt.MapClaims); ok {\n\t\t\ttoken, err = jwt.Parse(auth, cfg.KeyFunc)\n\t\t} else {\n\t\t\tt := reflect.ValueOf(cfg.Claims).Type().Elem()\n\t\t\tclaims := reflect.New(t).Interface().(jwt.Claims)\n\t\t\ttoken, err = jwt.ParseWithClaims(auth, claims, cfg.KeyFunc)\n\t\t}\n\t\tif err == nil && token.Valid {\n\t\t\t// Store user information from token into context.\n\t\t\tfiber.StoreInContext(c, tokenKey, token)\n\t\t\treturn cfg.SuccessHandler(c)\n\t\t}\n\t\treturn cfg.ErrorHandler(c, err)\n\t}\n}\n\n// FromContext returns the token from the context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// If there is no token, nil is returned.\nfunc FromContext(ctx any) *jwt.Token {\n\ttoken, ok := fiber.ValueFromContext[*jwt.Token](ctx, tokenKey)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn token\n}\n"
  },
  {
    "path": "v3/jwt/jwt_test.go",
    "content": "package jwtware_test\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n\t\"github.com/golang-jwt/jwt/v5\"\n\n\tjwtware \"github.com/gofiber/contrib/v3/jwt\"\n)\n\ntype TestToken struct {\n\tSigningMethod string\n\tToken         string\n}\n\nvar (\n\thamac = []TestToken{\n\t\t{\n\t\t\tSigningMethod: jwtware.HS256,\n\t\t\tToken:         \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o\",\n\t\t},\n\t\t{\n\t\t\tSigningMethod: jwtware.HS384,\n\t\t\tToken:         \"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hO2sthNQUSfvI9ylUdMKDxcrm8jB3KL6Rtkd3FOskL-jVqYh2CK1es8FKCQO8_tW\",\n\t\t},\n\t\t{\n\t\t\tSigningMethod: jwtware.HS512,\n\t\t\tToken:         \"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wUVS6tazE2N98_J4SH_djkEe1igXPu0qILAvVXCiO6O20gdf5vZ2sYFWX3c-Hy6L4TD47b3DSAAO9XjSqpJfag\",\n\t\t},\n\t}\n\n\trsa = []TestToken{\n\t\t{\n\t\t\tSigningMethod: jwtware.RS256,\n\t\t\tToken:         \"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.gvWLzl1sYUXdYqAPqFYLEJYtqPce8YxrV6LPiyWX2147llj1YfquFySnC8KOUTykCAxZHe6tFkyyZOp35HOqV3P-jxW2rw05mpNhld79f-O2sAFEzV7qxJXuYi4TL-Qn1gaLWP7i9B6B9c-0xLzYUmtLdrmlM2pxfPkXwG0oSao\",\n\t\t},\n\t\t{\n\t\t\tSigningMethod: jwtware.RS384,\n\t\t\tToken:         \"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.IIFu5jNRT5fIe91we3ARLTpE8hGu4tK6gsWtrJ1lAWzCxUYsVE02yOi3ya9RJsh-37GN8LdfVw74ZQzr4dwuq8SorycVatA2bc_OfkWpioOoPCqGMBFgsEdue0qtL1taflA-YSNG-Qntpqx_ciCGfI1DhiqikLaL-LSe8H9YOWk\",\n\t\t},\n\t\t{\n\t\t\tSigningMethod: jwtware.RS512,\n\t\t\tToken:         \"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.DKY-VXa6JJUZpupEUcmXETwaV2jfLydyeBfhSP8pIEW9g52fQ3g5hrHCNstxG2yy9yU68yrFqrBDetDX_yJ6qSHAOInwGWYot8W4D0lJvqsHJe0W0IPi03xiaWjwKO26xENCUzNNLvSPKPox5DPcg31gzCFBrIUgVX-TkpajuSE\",\n\t\t},\n\t}\n\n\tecdsa = []TestToken{\n\t\t{\n\t\t\tSigningMethod: jwtware.ES256,\n\t\t\tToken:         \"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC0yNTYifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.n6iJptkq2i6Y6gbuc92f2ExT9oXbg7hdMlR5MvkCZjayxBAyfpIGGoQAjMriwEs4rjF5F-DSU8T6eUcDxNhonA\",\n\t\t},\n\t\t{\n\t\t\tSigningMethod: jwtware.ES384,\n\t\t\tToken:         \"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC0zODQifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.WYGFC6NTSzD1E3Zv7Lyy3m_1l0zoF2tZqvDBxQBXqJN-bStTBzNYnpWZDMN6XMI7OqFbPGlh_Jff4Z4dlf0bieEfenURdtpoLIQI1zPNXoIfaY7TH8BTAXQKtoBk89Ed\",\n\t\t},\n\t\t{\n\t\t\tSigningMethod: jwtware.ES512,\n\t\t\tToken:         \"eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC01MjEifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ADwlteggILiCM_oCkxsyJTRK6BpQyH2FBQD_Tw_ph0vpLPRrpAkyh_CZIY9uZqqpb3J_eohscCzj5Vo9jrhP9DFRAdvLZCgehLj6N8P9aro2uy9jAl7kowxe0nEErv1SrD9qlyLWJh80jJVHRBVHXXysQ2WUD0KiRBq4x1p8jdEw5vHy\",\n\t\t},\n\t}\n)\n\nconst (\n\tdefaultSigningKey = \"secret\"\n\tdefaultKeySet     = `\n{\n   \"keys\":[\n\t   {\n\t\t\t\"e\": \"AQAB\",\n\t\t\t\"kid\": \"gofiber-rsa\",\n\t\t\t\"kty\": \"RSA\",\n\t\t\t\"n\": \"2IPZysef6KVySrb_RPopuwWy1C7KRfE96zQ9jIRwPghlvs0yfj9VK4rqeYbuHp5k9ghbjm1Bn2LMLR-JzqYWbchxzVrV58ay4nRHYUSjyzdbNcG0J4W-NxHnVqK0UUOl59uikRDqGHh3eRen_jVO_B8lvhqM57HQhA-czHbsmeU\"\n\t\t},\n\t\t{\n\t\t\t\"crv\": \"P-256\",\n\t\t\t\"kid\": \"gofiber-p-256\",\n\t\t\t\"kty\": \"EC\",\n\t\t\t\"x\": \"nLZJMz-8B6p2A1-owmTrCZqZx87_Y5soNPW74dQ8EDw\",\n\t\t\t\"y\": \"RvuLyi0tS-Tcx35IMy6aL_ID0K-cJFXmkFR8t9XJ4pc\"\n\t\t},\n\t\t{\n\t\t\t\"crv\": \"P-384\",\n\t\t\t\"kid\": \"gofiber-p-384\",\n\t\t\t\"kty\": \"EC\",\n\t\t\t\"x\": \"wvSt-v7az1qbz493ToTSvNcXgdIGqTtlcLzW7B1Ko3QWVgmtBYWQr_Q311_QX9DY\",\n\t\t\t\"y\": \"DvvBgCVjsDyttGAF8cmTP5maV46PrxACZFLvC1OEiZh-Ul0obSGXqG2xu8ulINPy\"\n\t\t},\n\t\t{\n\t\t\t\"crv\": \"P-521\",\n\t\t\t\"kid\": \"gofiber-p-521\",\n\t\t\t\"kty\": \"EC\",\n\t\t\t\"x\": \"AZhzdsnk9Dx5fLdPDnYJOI3ClkghbyFvpSq2ExzyPNgjZz_7iBUjyyLtr6QDn9BAaeFvSQFHvhZUylIQZ9wdIinq\",\n\t\t\t\"y\": \"AC2Me0tRqydVv7d23_0xdjiDndGuk0XpSZL5jeDWQ1_Tuty28-pJrFx38QQmWnosC0lBEdOUjxq-71YP7e4TzRMR\"\n\t\t}\n\t]\n}\n`\n)\n\nfunc TestJwtTokenProcessorFunc(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err != nil {\n\t\t\tt.Fatalf(\"Middleware should not panic\")\n\t\t}\n\t}()\n\n\tfor _, test := range hamac {\n\t\t// Arrange\n\t\tapp := fiber.New()\n\n\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\tSigningKey: jwtware.SigningKey{\n\t\t\t\tJWTAlg: test.SigningMethod,\n\t\t\t\tKey:    []byte(defaultSigningKey),\n\t\t\t},\n\t\t\tTokenProcessorFunc: func(token string) (string, error) {\n\t\t\t\tdecodedToken, err := hex.DecodeString(token)\n\t\t\t\treturn string(decodedToken), err\n\t\t\t},\n\t\t}))\n\n\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\treq.Header.Add(\"Authorization\", \"Bearer \"+hex.EncodeToString([]byte(test.Token)))\n\n\t\t// Act\n\t\tresp, err := app.Test(req)\n\n\t\t// Assert\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, 200, resp.StatusCode)\n\t}\n}\n\nfunc TestJwtFromHeader(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err != nil {\n\t\t\tt.Fatalf(\"Middleware should not panic\")\n\t\t}\n\t}()\n\n\tt.Run(\"regular\", func(t *testing.T) {\n\t\tfor _, test := range hamac {\n\t\t\t// Arrange\n\t\t\tapp := fiber.New()\n\n\t\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\t\tSigningKey: jwtware.SigningKey{\n\t\t\t\t\tJWTAlg: test.SigningMethod,\n\t\t\t\t\tKey:    []byte(defaultSigningKey),\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"OK\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\t\treq.Header.Add(\"Authorization\", \"Bearer \"+test.Token)\n\n\t\t\t// Act\n\t\t\tresp, err := app.Test(req)\n\n\t\t\t// Assert\n\t\t\tassert.Equal(t, nil, err)\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t}\n\t})\n\n\tt.Run(\"custom\", func(t *testing.T) {\n\t\tfor _, test := range hamac {\n\t\t\t// Arrange\n\t\t\tapp := fiber.New()\n\n\t\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\t\tSigningKey: jwtware.SigningKey{\n\t\t\t\t\tJWTAlg: test.SigningMethod,\n\t\t\t\t\tKey:    []byte(defaultSigningKey),\n\t\t\t\t},\n\t\t\t\tExtractor: extractors.FromHeader(\"X-Token\"),\n\t\t\t}))\n\n\t\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"OK\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\t\treq.Header.Set(\"X-Token\", test.Token)\n\n\t\t\t// Act\n\t\t\tresp, err := app.Test(req)\n\n\t\t\t// Assert\n\t\t\tassert.Equal(t, nil, err)\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t}\n\t})\n\n\tt.Run(\"malformed header\", func(t *testing.T) {\n\t\tfor _, test := range hamac {\n\t\t\t// Arrange\n\t\t\tapp := fiber.New()\n\n\t\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\t\tSigningKey: jwtware.SigningKey{\n\t\t\t\t\tJWTAlg: test.SigningMethod,\n\t\t\t\t\tKey:    []byte(defaultSigningKey),\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\t\treturn c.SendString(\"OK\")\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\t\treq.Header.Add(\"Authorization\", \"Bearer\"+test.Token)\n\n\t\t\t// Act\n\t\t\tresp, err := app.Test(req)\n\n\t\t\t// Assert\n\t\t\tassert.Equal(t, nil, err)\n\t\t\tassert.Equal(t, 400, resp.StatusCode)\n\t\t}\n\t})\n}\n\nfunc TestJwtFromCookie(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err != nil {\n\t\t\tt.Fatalf(\"Middleware should not panic\")\n\t\t}\n\t}()\n\n\tfor _, test := range hamac {\n\t\t// Arrange\n\t\tapp := fiber.New()\n\n\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\tSigningKey: jwtware.SigningKey{\n\t\t\t\tJWTAlg: test.SigningMethod,\n\t\t\t\tKey:    []byte(defaultSigningKey),\n\t\t\t},\n\t\t\tExtractor: extractors.FromCookie(\"Token\"),\n\t\t}))\n\n\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\tcookie := &http.Cookie{\n\t\t\tName:  \"Token\",\n\t\t\tValue: test.Token,\n\t\t}\n\t\treq.AddCookie(cookie)\n\n\t\t// Act\n\t\tresp, err := app.Test(req)\n\n\t\t// Assert\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, 200, resp.StatusCode)\n\t}\n}\n\n// TestJWKs performs a table test on the JWKs code.\n// deprecated\nfunc TestJwkFromServer(t *testing.T) {\n\t// Could add a test with an invalid JWKs endpoint.\n\t// Create a temporary directory to serve the JWKs from.\n\ttempDir, err := os.MkdirTemp(\"\", \"*\")\n\tif err != nil {\n\t\tt.Errorf(\"Failed to create a temporary directory.\\nError:%s\\n\", err.Error())\n\t\tt.FailNow()\n\t}\n\tdefer func() {\n\t\tif err = os.RemoveAll(tempDir); err != nil {\n\t\t\tt.Errorf(\"Failed to remove temporary directory.\\nError:%s\\n\", err.Error())\n\t\t\tt.FailNow()\n\t\t}\n\t}()\n\n\t// Create the JWKs file path.\n\tjwksFile := filepath.Join(tempDir, \"jwks.json\")\n\n\t// Write the empty JWKs.\n\tif err = os.WriteFile(jwksFile, []byte(defaultKeySet), 0600); err != nil {\n\t\tt.Errorf(\"Failed to write JWKs file to temporary directory.\\nError:%s\\n\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\t// Create the HTTP test server.\n\tserver := httptest.NewServer(http.FileServer(http.Dir(tempDir)))\n\tdefer server.Close()\n\n\t// Iterate through the test cases.\n\tfor _, test := range append(rsa, ecdsa...) {\n\t\t// Arrange\n\t\tapp := fiber.New()\n\n\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\tJWKSetURLs: []string{server.URL + \"/jwks.json\"},\n\t\t}))\n\n\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\treq.Header.Add(\"Authorization\", \"Bearer \"+test.Token)\n\n\t\t// Act\n\t\tresp, err := app.Test(req)\n\n\t\t// Assert\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, 200, resp.StatusCode)\n\t}\n}\n\n// TestJWKs performs a table test on the JWKs code.\nfunc TestJwkFromServers(t *testing.T) {\n\t// Could add a test with an invalid JWKs endpoint.\n\t// Create a temporary directory to serve the JWKs from.\n\ttempDir, err := os.MkdirTemp(\"\", \"*\")\n\tif err != nil {\n\t\tt.Errorf(\"Failed to create a temporary directory.\\nError:%s\\n\", err.Error())\n\t\tt.FailNow()\n\t}\n\tdefer func() {\n\t\tif err = os.RemoveAll(tempDir); err != nil {\n\t\t\tt.Errorf(\"Failed to remove temporary directory.\\nError:%s\\n\", err.Error())\n\t\t\tt.FailNow()\n\t\t}\n\t}()\n\n\t// Create the JWKs file path.\n\tjwksFile := filepath.Join(tempDir, \"jwks.json\")\n\tjwksFile2 := filepath.Join(tempDir, \"jwks2.json\")\n\n\t// Write the empty JWKs.\n\tif err = os.WriteFile(jwksFile, []byte(defaultKeySet), 0600); err != nil {\n\t\tt.Errorf(\"Failed to write JWKs file to temporary directory.\\nError:%s\\n\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\t// Write the empty JWKs 2.\n\tif err = os.WriteFile(jwksFile2, []byte(defaultKeySet), 0600); err != nil {\n\t\tt.Errorf(\"Failed to write JWKs file to temporary directory.\\nError:%s\\n\", err.Error())\n\t\tt.FailNow()\n\t}\n\n\t// Create the HTTP test server.\n\tserver := httptest.NewServer(http.FileServer(http.Dir(tempDir)))\n\tdefer server.Close()\n\n\t// Iterate through the test cases.\n\tfor _, test := range append(rsa, ecdsa...) {\n\t\t// Arrange\n\t\tapp := fiber.New()\n\n\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\tJWKSetURLs: []string{server.URL + \"/jwks.json\", server.URL + \"/jwks2.json\"},\n\t\t}))\n\n\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\treq.Header.Add(\"Authorization\", \"Bearer \"+test.Token)\n\n\t\t// Act\n\t\tresp, err := app.Test(req)\n\n\t\t// Assert\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, 200, resp.StatusCode)\n\t}\n}\n\nfunc TestCustomKeyfunc(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err != nil {\n\t\t\tt.Fatalf(\"Middleware should not panic\")\n\t\t}\n\t}()\n\n\ttest := hamac[0]\n\t// Arrange\n\tapp := fiber.New()\n\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tKeyFunc: customKeyfunc(),\n\t}))\n\n\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+test.Token)\n\n\t// Act\n\tresp, err := app.Test(req)\n\n\t// Assert\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n}\n\nfunc TestMultiKeys(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err != nil {\n\t\t\tt.Fatalf(\"Middleware should not panic\")\n\t\t}\n\t}()\n\n\tsignMethod := jwt.SigningMethodHS512\n\n\tkeys := map[string]jwtware.SigningKey{\n\t\t\"1\": {\n\t\t\tJWTAlg: signMethod.Name,\n\t\t\tKey:    []byte(\"aaa\"),\n\t\t},\n\t\t\"2\": {\n\t\t\tJWTAlg: signMethod.Name,\n\t\t\tKey:    []byte(\"bbb\"),\n\t\t},\n\t}\n\n\ttestTokens := make(map[string]string)\n\tfor kid, key := range keys {\n\t\ttoken := jwt.New(signMethod)\n\t\ttoken.Header[\"kid\"] = kid\n\t\ttokenStr, err := token.SignedString(key.Key)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\ttestTokens[kid] = tokenStr\n\t}\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKeys: keys,\n\t}))\n\n\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\tfor _, tokenStr := range testTokens {\n\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\treq.Header.Add(\"Authorization\", \"Bearer \"+tokenStr)\n\n\t\t// Act\n\t\tresp, err := app.Test(req)\n\n\t\t// Assert\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, 200, resp.StatusCode)\n\t}\n}\n\nfunc customKeyfunc() jwt.Keyfunc {\n\treturn func(t *jwt.Token) (interface{}, error) {\n\t\t// Always check the signing method\n\t\tif t.Method.Alg() != jwtware.HS256 {\n\t\t\treturn nil, fmt.Errorf(\"Unexpected jwt signing method=%v\", t.Header[\"alg\"])\n\t\t}\n\n\t\treturn []byte(defaultSigningKey), nil\n\t}\n}\n\nfunc TestFromContext(t *testing.T) {\n\tt.Parallel()\n\n\tdefer func() {\n\t\t// Assert\n\t\tif err := recover(); err != nil {\n\t\t\tt.Fatalf(\"Middleware should not panic\")\n\t\t}\n\t}()\n\n\tfor _, test := range hamac {\n\t\t// Arrange\n\t\tapp := fiber.New()\n\n\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\tSigningKey: jwtware.SigningKey{\n\t\t\t\tJWTAlg: test.SigningMethod,\n\t\t\t\tKey:    []byte(defaultSigningKey),\n\t\t\t},\n\t\t}))\n\n\t\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\t\ttoken := jwtware.FromContext(c)\n\t\t\tif token == nil {\n\t\t\t\treturn c.SendStatus(fiber.StatusUnauthorized)\n\t\t\t}\n\t\t\treturn c.SendString(\"OK\")\n\t\t})\n\n\t\treq := httptest.NewRequest(\"GET\", \"/ok\", nil)\n\t\treq.Header.Add(\"Authorization\", \"Bearer \"+test.Token)\n\n\t\t// Act\n\t\tresp, err := app.Test(req)\n\n\t\t// Assert\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 200, resp.StatusCode)\n\t}\n}\n\nfunc TestCustomErrorHandler(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tcustomErrorCalled := false\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)},\n\t\tErrorHandler: func(c fiber.Ctx, err error) error {\n\t\t\tcustomErrorCalled = true\n\t\t\treturn c.Status(fiber.StatusTeapot).SendString(\"Custom Error: \" + err.Error())\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/protected\", nil)\n\treq.Header.Add(\"Authorization\", \"Bearer invalid.token.here\")\n\n\t// Act\n\tresp, err := app.Test(req)\n\n\t// Assert\n\tassert.NoError(t, err)\n\tassert.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\tb, _ := io.ReadAll(resp.Body)\n\tassert.Contains(t, string(b), \"Custom Error:\")\n\tassert.True(t, customErrorCalled)\n}\n\nfunc TestCustomSuccessHandler(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tcustomSuccessCalled := false\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)},\n\t\tSuccessHandler: func(c fiber.Ctx) error {\n\t\t\tcustomSuccessCalled = true\n\t\t\tc.Locals(\"custom\", \"success\")\n\t\t\treturn c.Next()\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(c fiber.Ctx) error {\n\t\tif c.Locals(\"custom\") == \"success\" {\n\t\t\treturn c.SendString(\"Custom Success Handler Worked\")\n\t\t}\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/protected\", nil)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+hamac[0].Token)\n\n\t// Act\n\tresp, err := app.Test(req)\n\n\t// Assert\n\tassert.NoError(t, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tb, _ := io.ReadAll(resp.Body)\n\tassert.Equal(t, \"Custom Success Handler Worked\", string(b))\n\tassert.True(t, customSuccessCalled)\n}\n\nfunc TestNextFunction(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)},\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Get(\"Skip-JWT\") == \"true\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Should not reach here\")\n\t})\n\n\tapp.Get(\"/skipped\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"Skipped JWT\")\n\t})\n\n\t// Test skipping JWT\n\treq := httptest.NewRequest(\"GET\", \"/skipped\", nil)\n\treq.Header.Add(\"Skip-JWT\", \"true\")\n\tresp, err := app.Test(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tbody, _ := io.ReadAll(resp.Body)\n\tassert.Equal(t, \"Skipped JWT\", string(body))\n\n\t// Test not skipping JWT (should fail without token)\n\treq2 := httptest.NewRequest(\"GET\", \"/protected\", nil)\n\tresp2, err2 := app.Test(req2)\n\tassert.NoError(t, err2)\n\tassert.Equal(t, 400, resp2.StatusCode)\n}\n\nfunc TestInvalidSigningKey(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Panics(t, func() {\n\t\tapp := fiber.New()\n\t\tapp.Use(jwtware.New(jwtware.Config{\n\t\t\tSigningKey: jwtware.SigningKey{Key: nil}, // Invalid key\n\t\t}))\n\t}, \"Middleware should panic on invalid signing key\")\n}\n\nfunc TestFromContextWithoutToken(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tapp.Get(\"/no-jwt\", func(c fiber.Ctx) error {\n\t\ttoken := jwtware.FromContext(c)\n\t\tif token == nil {\n\t\t\treturn c.SendString(\"No token as expected\")\n\t\t}\n\t\treturn c.SendString(\"Unexpected token\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/no-jwt\", nil)\n\n\t// Act\n\tresp, err := app.Test(req)\n\n\t// Assert\n\tassert.NoError(t, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n}\n\nfunc TestMalformedToken(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)},\n\t}))\n\n\tapp.Get(\"/protected\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/protected\", nil)\n\treq.Header.Add(\"Authorization\", \"Bearer not.a.jwt.token\")\n\n\t// Act\n\tresp, err := app.Test(req)\n\n\t// Assert\n\tassert.NoError(t, err)\n\tassert.Equal(t, 401, resp.StatusCode)\n}\n\nfunc TestTokenProcessorFuncError(t *testing.T) {\n\tt.Parallel()\n\n\t// Arrange\n\tapp := fiber.New()\n\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)},\n\t\tTokenProcessorFunc: func(token string) (string, error) {\n\t\t\treturn \"\", fiber.NewError(fiber.StatusBadRequest, \"Token processing failed\")\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"OK\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/protected\", nil)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+hamac[0].Token)\n\n\t// Act\n\tresp, err := app.Test(req)\n\n\t// Assert\n\tassert.NoError(t, err)\n\tassert.Equal(t, 400, resp.StatusCode)\n}\n\nfunc TestFromContext_PassLocalsToContext(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\n\tapp.Use(jwtware.New(jwtware.Config{\n\t\tSigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)},\n\t}))\n\n\tapp.Get(\"/ok\", func(c fiber.Ctx) error {\n\t\ttokenFromCtx := jwtware.FromContext(c)\n\t\ttokenFromStdCtx := jwtware.FromContext(c.Context())\n\t\tif tokenFromCtx == nil || tokenFromStdCtx == nil {\n\t\t\treturn c.SendStatus(fiber.StatusUnauthorized)\n\t\t}\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/ok\", nil)\n\treq.Header.Add(\"Authorization\", \"Bearer \"+hamac[0].Token)\n\n\tresp, err := app.Test(req)\n\tassert.NoError(t, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n"
  },
  {
    "path": "v3/loadshed/README.md",
    "content": "---\nid: loadshed\n---\n\n# LoadShed\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*loadshed*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20Loadshed/badge.svg)\n\nThe LoadShed middleware for [Fiber](https://github.com/gofiber/fiber) is designed to help manage server load by shedding requests based on certain load criteria.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/loadshed\n```\n\n## Signatures\n\n```go\nloadshed.New(config ...loadshed.Config) fiber.Handler\n```\n\n## Examples\n\nTo use the LoadShed middleware in your Fiber application, import it and apply it to your Fiber app. Here's an example:\n\n### Basic\n\n```go\npackage main\n\nimport (\n  \"time\"\n  \"github.com/gofiber/fiber/v3\"\n  loadshed \"github.com/gofiber/contrib/v3/loadshed\"\n)\n\nfunc main() {\n  app := fiber.New()\n\n  // Configure and use LoadShed middleware\n  app.Use(loadshed.New(loadshed.Config{\n    Criteria: &loadshed.CPULoadCriteria{\n      LowerThreshold: 0.75, // Set your own lower threshold\n      UpperThreshold: 0.90, // Set your own upper threshold\n      Interval:       10 * time.Second,\n      Getter:         &loadshed.DefaultCPUPercentGetter{},\n    },\n  }))\n\n  app.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Welcome!\")\n  })\n\n  app.Listen(\":3000\")\n}\n```\n\n### With a custom rejection handler\n\n```go\npackage main\n\nimport (\n  \"time\"\n  \"github.com/gofiber/fiber/v3\"\n  loadshed \"github.com/gofiber/contrib/v3/loadshed\"\n)\n\nfunc main() {\n  app := fiber.New()\n\n  // Configure and use LoadShed middleware\n  app.Use(loadshed.New(loadshed.Config{\n    Criteria: &loadshed.CPULoadCriteria{\n      LowerThreshold: 0.75, // Set your own lower threshold\n      UpperThreshold: 0.90, // Set your own upper threshold\n      Interval:       10 * time.Second,\n      Getter:         &loadshed.DefaultCPUPercentGetter{},\n    },\n    OnShed: func(ctx fiber.Ctx) error {\n      if ctx.Method() == fiber.MethodGet {\n        return ctx.\n          Status(fiber.StatusTooManyRequests).\n          Send([]byte{})\n      }\n\n      return ctx.\n        Status(fiber.StatusTooManyRequests).\n        JSON(fiber.Map{\n          \"error\": \"Keep calm\",\n        })\n    },\n  }))\n\n  app.Get(\"/\", func(c fiber.Ctx) error {\n    return c.SendString(\"Welcome!\")\n  })\n\n  app.Listen(\":3000\")\n}\n```\n\n## Config\n\nThe LoadShed middleware in Fiber offers various configuration options to tailor the load shedding behavior according to the needs of your application.\n\n| Property | Type                       | Description                                             | Default                 |\n|:---------|:---------------------------|:--------------------------------------------------------|:------------------------|\n| Next     | `func(fiber.Ctx) bool`    | Function to skip this middleware when returned true.    | `nil`                   |\n| Criteria | `LoadCriteria`             | Interface for defining load shedding criteria.          | `&CPULoadCriteria{...}` |\n| OnShed   | `func(c fiber.Ctx) error` | Function to be executed if a request should be declined | `nil`                   |\n\n## LoadCriteria\n\nLoadCriteria is an interface in the LoadShed middleware that defines the criteria for determining when to shed load in the system. Different implementations of this interface can use various metrics and algorithms to decide when and how to shed incoming requests to maintain system performance.\n\n### CPULoadCriteria\n\n`CPULoadCriteria` is an implementation of the `LoadCriteria` interface, using CPU load as the metric for determining whether to shed requests.\n\n#### Properties\n\n| Property       | Type               | Description                                                                                                                           |\n|:---------------|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------|\n| LowerThreshold | `float64`          | The lower CPU usage threshold as a fraction (0.0 to 1.0). Requests are considered for shedding when CPU usage exceeds this threshold. |\n| UpperThreshold | `float64`          | The upper CPU usage threshold as a fraction (0.0 to 1.0). All requests are shed when CPU usage exceeds this threshold.                |\n| Interval       | `time.Duration`    | The time interval over which the CPU usage is averaged for decision making.                                                           |\n| Getter         | `CPUPercentGetter` | Interface to retrieve CPU usage percentages.                                                                                          |\n\n#### How It Works\n\n`CPULoadCriteria` determines the load on the system based on CPU usage and decides whether to shed incoming requests. It operates on the following principles:\n\n- **CPU Usage Measurement**: It measures the CPU usage over a specified interval.\n- **Thresholds**: Utilizes `LowerThreshold` and `UpperThreshold` values to decide when to start shedding requests.\n- **Proportional Rejection Probability**:\n  - **Below `LowerThreshold`**: No requests are rejected, as the system is considered under acceptable load.\n  - **Between `LowerThreshold` and `UpperThreshold`**: The probability of rejecting a request increases as the CPU usage approaches the `UpperThreshold`. This is calculated using the formula:\n```plaintext\n    rejectionProbability := (cpuUsage - LowerThreshold*100) / (UpperThreshold - LowerThreshold)\n```\n  - **Above `UpperThreshold`**: All requests are rejected to prevent system overload.\n\nThis mechanism ensures that the system can adaptively manage its load, maintaining stability and performance under varying traffic conditions.\n\n## Default Config\n\nThis is the default configuration for `LoadCriteria` in the LoadShed middleware.\n\n```go\nvar ConfigDefault = Config{\n  Next: nil,\n  Criteria: &CPULoadCriteria{\n    LowerThreshold: 0.90, // 90% CPU usage as the start point for considering shedding\n    UpperThreshold: 0.95, // 95% CPU usage as the point where all requests are shed\n    Interval:       10 * time.Second, // CPU usage is averaged over 10 seconds\n    Getter:         &DefaultCPUPercentGetter{}, // Default method for getting CPU usage\n  }, \n  OnShed: nil,\n}\n```\n"
  },
  {
    "path": "v3/loadshed/cpu.go",
    "content": "package loadshed\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"math\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/shirou/gopsutil/cpu\"\n)\n\n// LoadCriteria interface for different types of load metrics.\ntype LoadCriteria interface {\n\tMetric(ctx context.Context) (float64, error)\n\tShouldShed(metric float64) bool\n}\n\n// CPULoadCriteria for using CPU as a load metric.\ntype CPULoadCriteria struct {\n\tLowerThreshold float64\n\tUpperThreshold float64\n\tInterval       time.Duration\n\tGetter         CPUPercentGetter\n\n\tonce    sync.Once\n\tcached  atomic.Uint64\n\tlastErr atomic.Value // stores error; nil means \"no error\"\n\tcancel  context.CancelFunc\n}\n\n// minSamplerSleep is the minimum pause between sampler iterations to prevent\n// busy-spin when a custom getter returns instantly or the interval is tiny.\nconst minSamplerSleep = 100 * time.Millisecond\n\nfunc (c *CPULoadCriteria) startSampler() {\n\tctx, cancel := context.WithCancel(context.Background())\n\tc.cancel = cancel\n\n\tinterval := c.Interval\n\tif interval <= 0 {\n\t\tinterval = time.Second\n\t}\n\n\t// Mark cached as \"no sample yet\" so callers (e.g. waitForSample in tests)\n\t// can detect when the first real sample has been written.\n\tc.cached.Store(math.Float64bits(math.NaN()))\n\n\tgo func() {\n\t\t// Create a stopped, drained timer. We Reset it after each sample()\n\t\t// so the sleep duration is measured from the right point in time.\n\t\ttimer := time.NewTimer(0)\n\t\tif !timer.Stop() {\n\t\t\t<-timer.C\n\t\t}\n\t\tdefer func() {\n\t\t\tif !timer.Stop() {\n\t\t\t\tselect {\n\t\t\t\tcase <-timer.C:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tfor {\n\t\t\tstart := time.Now()\n\n\t\t\tc.sample(ctx, interval)\n\n\t\t\t// Calculate how long to sleep before the next sample.\n\t\t\telapsed := time.Since(start)\n\t\t\tsleep := interval - elapsed\n\t\t\tif sleep < minSamplerSleep {\n\t\t\t\tsleep = minSamplerSleep\n\t\t\t}\n\n\t\t\t// Safe to Reset: the channel was drained either by the initial\n\t\t\t// stop+drain above or by the previous <-timer.C in the select.\n\t\t\ttimer.Reset(sleep)\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-timer.C:\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// sample performs a single CPU measurement with panic recovery.\n// If the getter panics, it recovers and fails open (cached → 0) while\n// recording the underlying error for observability.\nfunc (c *CPULoadCriteria) sample(ctx context.Context, interval time.Duration) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\t// Fail open: treat CPU as idle so the middleware never sheds\n\t\t\t// based on a panicking getter, but record the panic as an error.\n\t\t\tc.cached.Store(math.Float64bits(0))\n\t\t\tc.lastErr.Store(fmt.Errorf(\"cpu percent getter panicked: %v\", r))\n\t\t}\n\t}()\n\n\tpercentages, err := c.Getter.PercentWithContext(ctx, interval, false)\n\tif err == nil && len(percentages) > 0 {\n\t\tc.cached.Store(math.Float64bits(percentages[0]))\n\t} else {\n\t\t// Fail open on sampling errors or empty results: treat CPU as\n\t\t// idle so the middleware never sheds based on stale high values.\n\t\tc.cached.Store(math.Float64bits(0))\n\n\t\t// Persist the underlying error so callers can observe persistent\n\t\t// sampling failures, even though the value fails open.\n\t\tif err != nil {\n\t\t\tc.lastErr.Store(err)\n\t\t} else {\n\t\t\tc.lastErr.Store(fmt.Errorf(\"cpu percent getter returned no samples\"))\n\t\t}\n\t}\n}\n\n// Stop terminates the background CPU sampler goroutine.\n// It is safe to call Stop concurrently and multiple times (context.CancelFunc\n// is idempotent). Stop also interrupts any in-progress CPU sampling call,\n// so shutdown is responsive regardless of the configured Interval.\n// If called before the sampler has started, no goroutine is ever launched.\nfunc (c *CPULoadCriteria) Stop() {\n\tc.once.Do(func() {\n\t\t// If Stop is called before Metric/New, set a no-op cancel without\n\t\t// starting the sampler goroutine. sync.Once guarantees that either\n\t\t// this func or startSampler runs, never both.\n\t\tc.cancel = func() {}\n\t})\n\tc.cancel()\n}\n\n// Metric returns the most recently sampled CPU usage percentage.\n// On the first call it starts a background goroutine that continuously\n// samples CPU usage at the configured Interval, so individual requests\n// are never blocked waiting for a CPU measurement.\n// Before the first sample completes, it returns 0 (allowing requests through).\n// On sampling errors the cached value is reset to 0, preserving fail-open\n// behaviour: ShouldShed(0) is always false, so requests are allowed through.\nfunc (c *CPULoadCriteria) Metric(ctx context.Context) (float64, error) {\n\tif err := ctx.Err(); err != nil {\n\t\t// Fail open: return a zero metric so requests are allowed through,\n\t\t// but surface the context error for observability.\n\t\t// Check before starting the sampler so a cancelled context doesn't\n\t\t// needlessly spin up the background goroutine.\n\t\treturn 0, err\n\t}\n\tc.once.Do(c.startSampler)\n\tvalue := math.Float64frombits(c.cached.Load())\n\tif math.IsNaN(value) {\n\t\t// Before the first successful sample, the cached value may be a NaN\n\t\t// sentinel. Expose this as 0 to preserve the documented fail-open\n\t\t// behaviour.\n\t\tvalue = 0\n\t}\n\treturn value, nil\n}\n\nfunc (c *CPULoadCriteria) ShouldShed(metric float64) bool {\n\tif metric > c.UpperThreshold*100 {\n\t\treturn true\n\t} else if metric > c.LowerThreshold*100 {\n\t\trejectionProbability := (metric - c.LowerThreshold*100) / (c.UpperThreshold - c.LowerThreshold)\n\t\t// #nosec G404\n\t\treturn rand.Float64()*100 < rejectionProbability\n\t}\n\treturn false\n}\n\ntype CPUPercentGetter interface {\n\tPercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error)\n}\n\ntype DefaultCPUPercentGetter struct{}\n\nfunc (*DefaultCPUPercentGetter) PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) {\n\treturn cpu.PercentWithContext(ctx, interval, percpu)\n}\n"
  },
  {
    "path": "v3/loadshed/go.mod",
    "content": "module github.com/gofiber/contrib/v3/loadshed\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/shirou/gopsutil v3.21.11+incompatible\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/loadshed/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=\ngithub.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/loadshed/loadshed.go",
    "content": "package loadshed\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n)\n\ntype Config struct {\n\t// Function to skip this middleware when returned true.\n\tNext func(c fiber.Ctx) bool\n\n\t// Criteria defines the criteria to be used for load shedding.\n\tCriteria LoadCriteria\n\n\t// OnShed defines a custom handler that will be executed if a request should\n\t// be rejected.\n\t//\n\t// Returning `nil` without writing to the response context allows the\n\t// request to proceed to the next handler\n\tOnShed func(c fiber.Ctx) error\n}\n\nvar ConfigDefault = Config{\n\tNext: nil,\n\tCriteria: &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       10 * time.Second, // Evaluate the average CPU usage over the last 10 seconds.\n\t\tGetter:         &DefaultCPUPercentGetter{},\n\t},\n}\n\nfunc configWithDefaults(config ...Config) Config {\n\tcfg := ConfigDefault\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\t}\n\t// Determine whether cfg.Criteria is the shared default pointer.\n\t// Use type assertion + pointer comparison instead of interface equality (==)\n\t// to avoid panics when a custom LoadCriteria holds a non-comparable type.\n\t// Also treat a typed-nil *CPULoadCriteria as unset so defaults are applied.\n\ttypedNilCPU := false\n\tisDefault := cfg.Criteria == nil\n\tif !isDefault {\n\t\tcfgCPU, cfgOK := cfg.Criteria.(*CPULoadCriteria)\n\t\tif cfgOK && cfgCPU == nil {\n\t\t\t// Typed-nil *CPULoadCriteria — treat as unset.\n\t\t\tisDefault = true\n\t\t\ttypedNilCPU = true\n\t\t} else {\n\t\t\tdefCPU, defOK := ConfigDefault.Criteria.(*CPULoadCriteria)\n\t\t\tisDefault = cfgOK && defOK && cfgCPU == defCPU\n\t\t}\n\t}\n\tif isDefault {\n\t\t// Clone the default CPULoadCriteria so each middleware instance has\n\t\t// its own sampler state (once/cached/cancel). This covers both the\n\t\t// no-args path (cfg inherits ConfigDefault.Criteria) and the\n\t\t// explicit Config{} path (Criteria is nil).\n\t\t// Use a guarded type assertion: users may replace ConfigDefault.Criteria\n\t\t// with a custom LoadCriteria implementation.\n\t\tif def, ok := ConfigDefault.Criteria.(*CPULoadCriteria); ok {\n\t\t\tcfg.Criteria = &CPULoadCriteria{\n\t\t\t\tLowerThreshold: def.LowerThreshold,\n\t\t\t\tUpperThreshold: def.UpperThreshold,\n\t\t\t\tInterval:       def.Interval,\n\t\t\t\tGetter:         def.Getter,\n\t\t\t}\n\t\t} else if cfg.Criteria == nil || typedNilCPU {\n\t\t\t// ConfigDefault.Criteria is a custom implementation; use it as-is.\n\t\t\tcfg.Criteria = ConfigDefault.Criteria\n\t\t}\n\t}\n\treturn cfg\n}\n\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configWithDefaults(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\t// Compute the load metric using the specified criteria\n\t\tmetric, err := cfg.Criteria.Metric(c.RequestCtx())\n\t\tif err != nil {\n\t\t\treturn c.Next() // If unable to get metric, allow the request\n\t\t}\n\n\t\t// Shed load if the criteria's ShouldShed method returns true\n\t\tif cfg.Criteria.ShouldShed(metric) {\n\t\t\t// Call the custom OnShed function\n\t\t\tif cfg.OnShed != nil {\n\t\t\t\treturn cfg.OnShed(c)\n\t\t\t}\n\n\t\t\treturn fiber.NewError(fiber.StatusServiceUnavailable)\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n"
  },
  {
    "path": "v3/loadshed/loadshed_test.go",
    "content": "package loadshed\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"math\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\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\n// waitForSample polls the criteria's cached value until the background\n// sampler has written a real sample (i.e., not NaN). The NaN sentinel is\n// set by startSampler itself, so this helper never clobbers an\n// already-populated cache — if the sampler has already completed a sample\n// before this function is called, it returns immediately.\nfunc waitForSample(t *testing.T, criteria *CPULoadCriteria) {\n\tt.Helper()\n\n\t// Ensure the sampler is running (triggers lazy start if needed).\n\tcriteria.once.Do(criteria.startSampler)\n\n\tinterval := criteria.Interval\n\tif interval <= 0 {\n\t\tinterval = time.Second\n\t}\n\n\trequire.Eventually(\n\t\tt,\n\t\tfunc() bool {\n\t\t\tv := math.Float64frombits(criteria.cached.Load())\n\t\t\treturn !math.IsNaN(v)\n\t\t},\n\t\t5*interval,\n\t\t10*time.Millisecond,\n\t\t\"timed out waiting for background sampler to populate cached metric\",\n\t)\n}\n\ntype MockCPUPercentGetter struct {\n\tMockedPercentage []float64\n}\n\nfunc (m *MockCPUPercentGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) {\n\treturn m.MockedPercentage, nil\n}\n\n// PanickingGetter panics on every call (for testing panic recovery).\ntype PanickingGetter struct{}\n\nfunc (*PanickingGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) {\n\tpanic(\"boom\")\n}\n\n// ErrorGetter always returns an error (for testing fail-open on errors).\ntype ErrorGetter struct{}\n\nfunc (*ErrorGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) {\n\treturn nil, context.DeadlineExceeded\n}\n\n// EmptyGetter returns an empty slice (for testing fail-open on empty results).\ntype EmptyGetter struct{}\n\nfunc (*EmptyGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) {\n\treturn []float64{}, nil\n}\n\nfunc ReturnOK(c fiber.Ctx) error {\n\treturn c.SendStatus(fiber.StatusOK)\n}\n\nfunc Test_Loadshed_LowerThreshold(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{89.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\n\tstatus := resp.StatusCode\n\tif status != fiber.StatusOK && status != fiber.StatusServiceUnavailable {\n\t\tt.Fatalf(\"Expected status code %d or %d but got %d\", fiber.StatusOK, fiber.StatusServiceUnavailable, status)\n\t}\n}\n\nfunc Test_Loadshed_DefaultCriteriaWhenNil(t *testing.T) {\n\tcfg := configWithDefaults(Config{})\n\n\t// configWithDefaults should clone the default CPULoadCriteria, not share it.\n\tcriteria, ok := cfg.Criteria.(*CPULoadCriteria)\n\trequire.True(t, ok)\n\tassert.NotSame(t, ConfigDefault.Criteria, criteria)\n\n\tdef := ConfigDefault.Criteria.(*CPULoadCriteria)\n\tassert.Equal(t, def.LowerThreshold, criteria.LowerThreshold)\n\tassert.Equal(t, def.UpperThreshold, criteria.UpperThreshold)\n\tassert.Equal(t, def.Interval, criteria.Interval)\n}\n\nfunc Test_Loadshed_DefaultCriteriaNoArgs(t *testing.T) {\n\tcfg := configWithDefaults()\n\n\t// The no-args path should also clone, not share the default singleton.\n\tcriteria, ok := cfg.Criteria.(*CPULoadCriteria)\n\trequire.True(t, ok)\n\tassert.NotSame(t, ConfigDefault.Criteria, criteria)\n\n\tdef := ConfigDefault.Criteria.(*CPULoadCriteria)\n\tassert.Equal(t, def.LowerThreshold, criteria.LowerThreshold)\n\tassert.Equal(t, def.UpperThreshold, criteria.UpperThreshold)\n\tassert.Equal(t, def.Interval, criteria.Interval)\n}\n\nfunc Test_Loadshed_MiddleValue(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{93.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\trejectedCount := 0\n\tacceptedCount := 0\n\titerations := 100000\n\n\tfor i := 0; i < iterations; i++ {\n\t\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\t\tassert.Equal(t, nil, err)\n\n\t\tif resp.StatusCode == fiber.StatusServiceUnavailable {\n\t\t\trejectedCount++\n\t\t} else {\n\t\t\tacceptedCount++\n\t\t}\n\t}\n\n\tt.Logf(\"Accepted: %d, Rejected: %d\", acceptedCount, rejectedCount)\n\tif acceptedCount == 0 || rejectedCount == 0 {\n\t\tt.Fatalf(\"Expected both accepted and rejected requests, but got Accepted: %d, Rejected: %d\", acceptedCount, rejectedCount)\n\t}\n}\n\nfunc Test_Loadshed_UpperThreshold(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)\n}\n\nfunc Test_Loadshed_CustomOnShed(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\tcfg.OnShed = func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusTooManyRequests).Send([]byte{})\n\t}\n\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\nfunc Test_Loadshed_CustomOnShedWithResponse(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\n\t// This OnShed directly sets a response without returning it\n\tcfg.OnShed = func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusTooManyRequests)\n\t\treturn nil\n\t}\n\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode)\n}\n\nfunc Test_Loadshed_CustomOnShedWithNilReturn(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\n\t// OnShed returns nil without setting a response\n\tcfg.OnShed = func(c fiber.Ctx) error {\n\t\treturn nil\n\t}\n\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_Loadshed_CustomOnShedWithCustomError(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\n\t// OnShed returns a custom error\n\tcfg.OnShed = func(c fiber.Ctx) error {\n\t\treturn fiber.NewError(fiber.StatusForbidden, \"Custom error message\")\n\t}\n\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusForbidden, resp.StatusCode)\n}\n\nfunc Test_Loadshed_CustomOnShedWithResponseAndCustomError(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\n\t// OnShed sets a response and returns a different error\n\t// The NewError have higher priority since executed last\n\tcfg.OnShed = func(c fiber.Ctx) error {\n\t\tc.\n\t\t\tStatus(fiber.StatusTooManyRequests).\n\t\t\tSendString(\"Too many requests\")\n\n\t\treturn fiber.NewError(\n\t\t\tfiber.StatusInternalServerError,\n\t\t\t\"Shed happened\",\n\t\t)\n\t}\n\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tpayload, readErr := io.ReadAll(resp.Body)\n\tdefer resp.Body.Close()\n\n\tassert.Equal(t, string(payload), \"Shed happened\")\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, nil, readErr)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n}\n\nfunc Test_Loadshed_CustomOnShedWithJSON(t *testing.T) {\n\tapp := fiber.New()\n\n\tmockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}}\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         mockGetter,\n\t}\n\tvar cfg Config\n\tcfg.Criteria = criteria\n\n\t// OnShed returns JSON response\n\tcfg.OnShed = func(c fiber.Ctx) error {\n\t\treturn c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{\n\t\t\t\"error\":       \"Service is currently unavailable due to high load\",\n\t\t\t\"retry_after\": 30,\n\t\t})\n\t}\n\n\tapp.Use(New(cfg))\n\tapp.Get(\"/\", ReturnOK)\n\tt.Cleanup(criteria.Stop)\n\twaitForSample(t, criteria)\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode)\n\tassert.Equal(t, \"application/json; charset=utf-8\", resp.Header.Get(\"Content-Type\"))\n}\n\nfunc Test_Loadshed_TypedNilCriteria(t *testing.T) {\n\t// A typed-nil *CPULoadCriteria assigned to the Criteria interface should\n\t// be treated as unset, and configWithDefaults should clone the default.\n\tvar nilCriteria *CPULoadCriteria\n\tcfg := configWithDefaults(Config{Criteria: nilCriteria})\n\n\tcriteria, ok := cfg.Criteria.(*CPULoadCriteria)\n\trequire.True(t, ok)\n\tassert.NotNil(t, criteria, \"typed-nil should be replaced with a cloned default\")\n\tassert.NotSame(t, ConfigDefault.Criteria, criteria)\n\n\tdef := ConfigDefault.Criteria.(*CPULoadCriteria)\n\tassert.Equal(t, def.LowerThreshold, criteria.LowerThreshold)\n\tassert.Equal(t, def.UpperThreshold, criteria.UpperThreshold)\n\tassert.Equal(t, def.Interval, criteria.Interval)\n}\n\nfunc Test_CPULoadCriteria_StopBeforeStart(t *testing.T) {\n\t// Stop() called before the sampler has been started should not panic\n\t// and should prevent the sampler from ever launching.\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         &MockCPUPercentGetter{MockedPercentage: []float64{50.0}},\n\t}\n\n\t// Should not panic.\n\tcriteria.Stop()\n\n\t// Metric should still work (returns 0, nil) without starting a sampler.\n\tmetric, err := criteria.Metric(context.Background())\n\tassert.NoError(t, err)\n\tassert.Equal(t, float64(0), metric)\n}\n\nfunc Test_CPULoadCriteria_PanickingGetter(t *testing.T) {\n\t// A getter that panics should not crash the process; the sampler\n\t// should recover and fail open (cached → 0).\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       100 * time.Millisecond,\n\t\tGetter:         &PanickingGetter{},\n\t}\n\tt.Cleanup(criteria.Stop)\n\n\t// Start the sampler, then use NaN sentinel to detect when it has run.\n\tcriteria.once.Do(criteria.startSampler)\n\twaitForSample(t, criteria)\n\n\t// The sampler should still be alive and returning 0 (fail-open).\n\tmetric, err := criteria.Metric(context.Background())\n\tassert.NoError(t, err)\n\tassert.Equal(t, float64(0), metric)\n}\n\nfunc Test_CPULoadCriteria_ErrorGetter(t *testing.T) {\n\t// A getter that returns errors should fail open (cached → 0).\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       100 * time.Millisecond,\n\t\tGetter:         &ErrorGetter{},\n\t}\n\tt.Cleanup(criteria.Stop)\n\n\tcriteria.once.Do(criteria.startSampler)\n\twaitForSample(t, criteria)\n\n\tmetric, err := criteria.Metric(context.Background())\n\tassert.NoError(t, err)\n\tassert.Equal(t, float64(0), metric)\n}\n\nfunc Test_CPULoadCriteria_EmptyGetter(t *testing.T) {\n\t// A getter that returns an empty slice should fail open (cached → 0).\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       100 * time.Millisecond,\n\t\tGetter:         &EmptyGetter{},\n\t}\n\tt.Cleanup(criteria.Stop)\n\n\tcriteria.once.Do(criteria.startSampler)\n\twaitForSample(t, criteria)\n\n\tmetric, err := criteria.Metric(context.Background())\n\tassert.NoError(t, err)\n\tassert.Equal(t, float64(0), metric)\n}\n\nfunc Test_CPULoadCriteria_MetricCancelledContext(t *testing.T) {\n\t// Metric() with a cancelled context should fail open (return 0)\n\t// but surface the context error for observability.\n\tcriteria := &CPULoadCriteria{\n\t\tLowerThreshold: 0.90,\n\t\tUpperThreshold: 0.95,\n\t\tInterval:       time.Second,\n\t\tGetter:         &MockCPUPercentGetter{MockedPercentage: []float64{50.0}},\n\t}\n\tt.Cleanup(criteria.Stop)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\n\tmetric, err := criteria.Metric(ctx)\n\tassert.ErrorIs(t, err, context.Canceled)\n\tassert.Equal(t, float64(0), metric)\n}\n"
  },
  {
    "path": "v3/monitor/README.md",
    "content": "---\nid: monitor\n---\n\n# Monitor\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*monitor*)\n![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20Monitor/badge.svg)\n\nMonitor middleware for [Fiber](https://github.com/gofiber/fiber) that reports server metrics, inspired by [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor)\n\n![](https://i.imgur.com/nHAtBpJ.gif)\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/monitor\n```\n\n### Signature\n\n```go\nmonitor.New(config ...monitor.Config) fiber.Handler\n```\n\n### Config\n\n| Property   | Type                      | Description                                                                          | Default                                                                     |\n| :--------- | :------------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |\n| Title      | `string`                  | Metrics page title.                                                                  | `Fiber Monitor`                                                             |\n| Refresh    | `time.Duration`           | Refresh period.                                                                      | `3 seconds`                                                                 |\n| APIOnly    | `bool`                    | Whether the service should expose only the montioring API.                           | `false`                                                                     |\n| Next       | `func(c fiber.Ctx) bool` | Define a function to add custom fields.                                              | `nil`                                                                       |\n| CustomHead | `string`                  | Custom HTML code to Head Section(Before End).                                        | `empty`                                                                     |\n| FontURL    | `string`                  | FontURL for specilt font resource path or URL. also you can use relative path.       | `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap` |\n| ChartJsURL | `string`                  | ChartJsURL for specilt chartjs library, path or URL, also you can use relative path. | `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js`        |\n\n### Example\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/contrib/v3/monitor\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Initialize default config (Assign the middleware to /metrics)\n    app.Get(\"/metrics\", monitor.New())\n\n    // Or extend your config for customization\n    // Assign the middleware to /metrics\n    // and change the Title to `MyService Metrics Page`\n    app.Get(\"/metrics\", monitor.New(monitor.Config{Title: \"MyService Metrics Page\"}))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## Default Config\n\n```go\nvar ConfigDefault = Config{\n    Title:      defaultTitle,\n    Refresh:    defaultRefresh,\n    FontURL:    defaultFontURL,\n    ChartJsURL: defaultChartJSURL,\n    CustomHead: defaultCustomHead,\n    APIOnly:    false,\n    Next:       nil,\n}\n```\n"
  },
  {
    "path": "v3/monitor/config.go",
    "content": "package monitor\n\nimport (\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// Metrics page title\n\t//\n\t// Optional. Default: \"Fiber Monitor\"\n\tTitle string\n\n\t// Refresh period\n\t//\n\t// Optional. Default: 3 seconds\n\tRefresh time.Duration\n\n\t// Whether the service should expose only the monitoring API.\n\t//\n\t// Optional. Default: false\n\tAPIOnly bool\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// Custom HTML Code to Head Section(Before End)\n\t//\n\t// Optional. Default: empty\n\tCustomHead string\n\n\t// FontURL to specify font resource path or URL. You can also use a relative path.\n\t//\n\t// Optional. Default: https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap\n\tFontURL string\n\n\t// ChartJSURL to specify ChartJS library path or URL. You can also use a relative path.\n\t//\n\t// Optional. Default: https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js\n\tChartJSURL string\n\n\tindex string\n}\n\nvar ConfigDefault = Config{\n\tTitle:      defaultTitle,\n\tRefresh:    defaultRefresh,\n\tFontURL:    defaultFontURL,\n\tChartJSURL: defaultChartJSURL,\n\tCustomHead: defaultCustomHead,\n\tAPIOnly:    false,\n\tNext:       nil,\n\tindex: newIndex(viewBag{\n\t\tdefaultTitle,\n\t\tdefaultRefresh,\n\t\tdefaultFontURL,\n\t\tdefaultChartJSURL,\n\t\tdefaultCustomHead,\n\t}),\n}\n\nfunc configDefault(config ...Config) Config {\n\t// Users can change ConfigDefault.Title/Refresh which then\n\t// become incompatible with ConfigDefault.index\n\tif ConfigDefault.Title != defaultTitle ||\n\t\tConfigDefault.Refresh != defaultRefresh ||\n\t\tConfigDefault.FontURL != defaultFontURL ||\n\t\tConfigDefault.ChartJSURL != defaultChartJSURL ||\n\t\tConfigDefault.CustomHead != defaultCustomHead {\n\t\tif ConfigDefault.Refresh < minRefresh {\n\t\t\tConfigDefault.Refresh = minRefresh\n\t\t}\n\t\t// update default index with new default title/refresh\n\t\tConfigDefault.index = newIndex(viewBag{\n\t\t\tConfigDefault.Title,\n\t\t\tConfigDefault.Refresh,\n\t\t\tConfigDefault.FontURL,\n\t\t\tConfigDefault.ChartJSURL,\n\t\t\tConfigDefault.CustomHead,\n\t\t})\n\t}\n\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.Title == \"\" {\n\t\tcfg.Title = ConfigDefault.Title\n\t}\n\n\tif cfg.Refresh == 0 {\n\t\tcfg.Refresh = ConfigDefault.Refresh\n\t}\n\tif cfg.FontURL == \"\" {\n\t\tcfg.FontURL = defaultFontURL\n\t}\n\n\tif cfg.ChartJSURL == \"\" {\n\t\tcfg.ChartJSURL = defaultChartJSURL\n\t}\n\tif cfg.Refresh < minRefresh {\n\t\tcfg.Refresh = minRefresh\n\t}\n\n\tif cfg.Next == nil {\n\t\tcfg.Next = ConfigDefault.Next\n\t}\n\n\tif !cfg.APIOnly {\n\t\tcfg.APIOnly = ConfigDefault.APIOnly\n\t}\n\n\t// update cfg.index with custom title/refresh\n\tcfg.index = newIndex(viewBag{\n\t\ttitle:      cfg.Title,\n\t\trefresh:    cfg.Refresh,\n\t\tfontURL:    cfg.FontURL,\n\t\tchartJSURL: cfg.ChartJSURL,\n\t\tcustomHead: cfg.CustomHead,\n\t})\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/monitor/config_test.go",
    "content": "package monitor\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_Config_Default(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"use default\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcfg := configDefault()\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set title\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttitle := \"title\"\n\t\tcfg := configDefault(Config{\n\t\t\tTitle: title,\n\t\t})\n\n\t\tassert.Equal(t, title, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{title, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set refresh less than default\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcfg := configDefault(Config{\n\t\t\tRefresh: 100 * time.Millisecond,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, minRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, minRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set refresh\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\trefresh := time.Second\n\t\tcfg := configDefault(Config{\n\t\t\tRefresh: refresh,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, refresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, refresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set font url\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tfontURL := \"https://example.com\"\n\t\tcfg := configDefault(Config{\n\t\t\tFontURL: fontURL,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, fontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, fontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set chart js url\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tchartURL := \"http://example.com\"\n\t\tcfg := configDefault(Config{\n\t\t\tChartJSURL: chartURL,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, chartURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, chartURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set custom head\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thead := \"head\"\n\t\tcfg := configDefault(Config{\n\t\t\tCustomHead: head,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, head, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, head}), cfg.index)\n\t})\n\n\tt.Run(\"set api only\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcfg := configDefault(Config{\n\t\t\tAPIOnly: true,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, true, cfg.APIOnly)\n\t\tassert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next)\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n\n\tt.Run(\"set next\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tf := func(c fiber.Ctx) bool {\n\t\t\treturn true\n\t\t}\n\t\tcfg := configDefault(Config{\n\t\t\tNext: f,\n\t\t})\n\n\t\tassert.Equal(t, defaultTitle, cfg.Title)\n\t\tassert.Equal(t, defaultRefresh, cfg.Refresh)\n\t\tassert.Equal(t, defaultFontURL, cfg.FontURL)\n\t\tassert.Equal(t, defaultChartJSURL, cfg.ChartJSURL)\n\t\tassert.Equal(t, defaultCustomHead, cfg.CustomHead)\n\t\tassert.Equal(t, false, cfg.APIOnly)\n\t\tassert.Equal(t, f(nil), cfg.Next(nil))\n\t\tassert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index)\n\t})\n}\n"
  },
  {
    "path": "v3/monitor/go.mod",
    "content": "module github.com/gofiber/contrib/v3/monitor\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/shirou/gopsutil/v4 v4.26.3\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/valyala/fasthttp v1.70.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/monitor/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=\ngithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/monitor/index.go",
    "content": "package monitor\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gofiber/utils/v2\"\n)\n\ntype viewBag struct {\n\ttitle      string\n\trefresh    time.Duration\n\tfontURL    string\n\tchartJSURL string\n\tcustomHead string\n}\n\n// returns index with new title/refresh\nfunc newIndex(dat viewBag) string {\n\ttimeout := dat.refresh.Milliseconds() - timeoutDiff\n\tif timeout < timeoutDiff {\n\t\ttimeout = timeoutDiff\n\t}\n\tts := utils.FormatInt(timeout)\n\treplacer := strings.NewReplacer(\"$TITLE\", dat.title, \"$TIMEOUT\", ts,\n\t\t\"$FONT_URL\", dat.fontURL, \"$CHART_JS_URL\", dat.chartJSURL, \"$CUSTOM_HEAD\", dat.customHead,\n\t)\n\treturn replacer.Replace(indexHTML)\n}\n\nconst (\n\tdefaultTitle = \"Fiber Monitor\"\n\n\tdefaultRefresh    = 3 * time.Second\n\ttimeoutDiff       = 200 // timeout will be Refresh (in milliseconds) - timeoutDiff\n\tminRefresh        = timeoutDiff * time.Millisecond\n\tdefaultFontURL    = `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap`\n\tdefaultChartJSURL = `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js`\n\tdefaultCustomHead = ``\n\n\t// parametrized by $TITLE and $TIMEOUT\n\tindexHTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<link href=\"$FONT_URL\" rel=\"stylesheet\">\n\t<script src=\"$CHART_JS_URL\"></script>\n\n\t<title>$TITLE</title>\n<style>\n\tbody {\n\t\tmargin: 0;\n\t\tfont: 16px / 1.6 'Roboto', sans-serif;\n\t}\n\t.wrapper {\n\t\tmax-width: 900px;\n\t\tmargin: 0 auto;\n\t\tpadding: 30px 0;\n\t}\n\t.title {\n\t\ttext-align: center;\n\t\tmargin-bottom: 2em;\n\t}\n\t.title h1 {\n\t\tfont-size: 1.8em;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t}\n\t.row {\n\t\tdisplay: flex;\n\t\tmargin-bottom: 20px;\n\t\talign-items: center;\n\t}\n\t.row .column:first-child { width: 35%; }\n\t.row .column:last-child { width: 65%; }\n\t.metric {\n\t\tcolor: #777;\n\t\tfont-weight: 900;\n\t}\n\th2 {\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\tfont-size: 2.2em;\n\t}\n\th2 span {\n\t\tfont-size: 12px;\n\t\tcolor: #777;\n\t}\n\th2 span.ram_os { color: rgba(255, 150, 0, .8); }\n\th2 span.ram_total { color: rgba(0, 200, 0, .8); }\n\tcanvas {\n\t\twidth: 200px;\n\t\theight: 180px;\n\t}\n$CUSTOM_HEAD\n</style>\n</head>\n<body>\n\t<section class=\"wrapper\">\n\t<div class=\"title\"><h1>$TITLE</h1></div>\n\t<section class=\"charts\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"column\">\n\t\t\t\t<div class=\"metric\">CPU Usage</div>\n\t\t\t\t<h2 id=\"cpuMetric\">0.00%</h2>\n\t\t\t</div>\n\t\t\t<div class=\"column\">\n\t\t\t\t<canvas id=\"cpuChart\"></canvas>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"row\">\n\t\t\t<div class=\"column\">\n\t\t\t\t<div class=\"metric\">Memory Usage</div>\n\t\t\t\t<h2 id=\"ramMetric\" title=\"PID used / OS used / OS total\">0.00 MB</h2>\n\t\t\t</div>\n\t\t\t<div class=\"column\">\n\t\t\t\t<canvas id=\"ramChart\"></canvas>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"row\">\n\t\t\t<div class=\"column\">\n\t\t\t\t<div class=\"metric\">Response Time</div>\n\t\t\t\t<h2 id=\"rtimeMetric\">0ms</h2>\n\t\t\t</div>\n\t\t\t<div class=\"column\">\n\t\t\t\t<canvas id=\"rtimeChart\"></canvas>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"row\">\n\t\t\t<div class=\"column\">\n\t\t\t\t<div class=\"metric\">Open Connections</div>\n\t\t\t\t<h2 id=\"connsMetric\">0</h2>\n\t\t\t</div>\n\t\t\t<div class=\"column\">\n\t\t\t\t<canvas id=\"connsChart\"></canvas>\n\t\t\t</div>\n\t\t</div>\n\t</section>\n\t</section>\n<script>\n\tfunction formatBytes(bytes, decimals = 1) {\n\t\tif (bytes === 0) return '0 Bytes';\n\n\t\tconst k = 1024;\n\t\tconst dm = decimals < 0 ? 0 : decimals;\n\t\tconst sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n\n\t\tconst i = Math.floor(Math.log(bytes) / Math.log(k));\n\n\t\treturn parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n\t}\n\tChart.defaults.global.legend.display = false;\n\tChart.defaults.global.defaultFontSize = 8;\n\tChart.defaults.global.animation.duration = 1000;\n\tChart.defaults.global.animation.easing = 'easeOutQuart';\n\tChart.defaults.global.elements.line.backgroundColor = 'rgba(0, 172, 215, 0.25)';\n\tChart.defaults.global.elements.line.borderColor = 'rgba(0, 172, 215, 1)';\n\tChart.defaults.global.elements.line.borderWidth = 2;\n\n\tconst options = {\n\t\tscales: {\n\t\t\tyAxes: [{ ticks: { beginAtZero: true }}],\n\t\t\txAxes: [{\n\t\t\t\ttype: 'time',\n\t\t\t\ttime: {\n\t\t\t\t\tunitStepSize: 30,\n\t\t\t\t\tunit: 'second'\n\t\t\t\t},\n\t\t\t\tgridlines: { display: false }\n\t\t\t}]\n\t\t},\n\t\ttooltips: {\tenabled: false },\n\t\tresponsive: true,\n\t\tmaintainAspectRatio: false,\n\t\tanimation: false\n\t};\n\tconst cpuMetric = document.querySelector('#cpuMetric');\n\tconst ramMetric = document.querySelector('#ramMetric');\n\tconst rtimeMetric = document.querySelector('#rtimeMetric');\n\tconst connsMetric = document.querySelector('#connsMetric');\n\n\tconst cpuChartCtx = document.querySelector('#cpuChart').getContext('2d');\n\tconst ramChartCtx = document.querySelector('#ramChart').getContext('2d');\n\tconst rtimeChartCtx = document.querySelector('#rtimeChart').getContext('2d');\n\tconst connsChartCtx = document.querySelector('#connsChart').getContext('2d');\n\n\tconst cpuChart = createChart(cpuChartCtx);\n\tconst ramChart = createChart(ramChartCtx);\n\tconst rtimeChart = createChart(rtimeChartCtx);\n\tconst connsChart = createChart(connsChartCtx);\n\n\tconst charts = [cpuChart, ramChart, rtimeChart, connsChart];\n\n\tfunction createChart(ctx) {\n\t\treturn new Chart(ctx, {\n\t\t\ttype: 'line',\n\t\t\tdata: {\n\t\t\t\tlabels: [],\n\t\t\t\tdatasets: [{\n\t\t\t\t\tlabel: '',\n\t\t\t\t\tdata: [],\n\t\t\t\t\tlineTension: 0.2,\n\t\t\t\t\tpointRadius: 0,\n\t\t\t\t}]\n\t\t\t},\n\t\t\toptions\n\t\t});\n\t}\n\tramChart.data.datasets.push({\n\t\tdata: [],\n\t\tlineTension: 0.2,\n\t\tpointRadius: 0,\n\t\tbackgroundColor: 'rgba(255, 200, 0, .6)',\n\t\tborderColor: 'rgba(255, 150, 0, .8)',\n\t})\n\tramChart.data.datasets.push({\n\t\tdata: [],\n\t\tlineTension: 0.2,\n\t\tpointRadius: 0,\n\t\tbackgroundColor: 'rgba(0, 255, 0, .4)',\n\t\tborderColor: 'rgba(0, 200, 0, .8)',\n\t})\n\tfunction update(json, rtime) {\n\t\tcpu = json.pid.cpu.toFixed(1);\n\t\tcpuOS = json.os.cpu.toFixed(1);\n\n\t\tcpuMetric.innerHTML = cpu + '% <span>' + cpuOS + '%</span>';\n\t\tramMetric.innerHTML = formatBytes(json.pid.ram) + '<span> / </span><span class=\"ram_os\">' + formatBytes(json.os.ram) +\n\t\t\t'<span><span> / </span><span class=\"ram_total\">' + formatBytes(json.os.total_ram) + '</span>';\n\t\trtimeMetric.innerHTML = rtime + 'ms <span>client</span>';\n\t\tconnsMetric.innerHTML = json.pid.conns + ' <span>' + json.os.conns + '</span>';\n\n\t\tcpuChart.data.datasets[0].data.push(cpu);\n\t\tramChart.data.datasets[2].data.push((json.os.total_ram / 1e6).toFixed(2));\n\t\tramChart.data.datasets[1].data.push((json.os.ram / 1e6).toFixed(2));\n\t\tramChart.data.datasets[0].data.push((json.pid.ram / 1e6).toFixed(2));\n\t\trtimeChart.data.datasets[0].data.push(rtime);\n\t\tconnsChart.data.datasets[0].data.push(json.pid.conns);\n\n\t\tconst timestamp = new Date().getTime();\n\n\t\tcharts.forEach(chart => {\n\t\t\tif (chart.data.labels.length > 50) {\n\t\t\t\tchart.data.datasets.forEach(function (dataset) { dataset.data.shift(); });\n\t\t\t\tchart.data.labels.shift();\n\t\t\t}\n\t\t\tchart.data.labels.push(timestamp);\n\t\t\tchart.update();\n\t\t});\n\t\tsetTimeout(fetchJSON, $TIMEOUT)\n\t}\n\tfunction fetchJSON() {\n\t\tvar t1 = ''\n\t\tvar t0 = performance.now()\n\t\tfetch(window.location.href, {\n\t\t\t\theaders: { 'Accept': 'application/json' },\n\t\t\t\tcredentials: 'same-origin'\n\t\t\t})\n\t\t\t.then(res => {\n\t\t\t\tt1 = performance.now()\n\t\t\t\treturn res.json()\n\t\t\t})\n\t\t\t.then(res => { update(res, Math.round(t1 - t0)) })\n\t\t\t.catch(console.error);\n\t}\n\tfetchJSON()\n</script>\n</body>\n</html>\n`\n)\n"
  },
  {
    "path": "v3/monitor/monitor.go",
    "content": "package monitor\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/shirou/gopsutil/v4/cpu\"\n\t\"github.com/shirou/gopsutil/v4/load\"\n\t\"github.com/shirou/gopsutil/v4/mem\"\n\t\"github.com/shirou/gopsutil/v4/net\"\n\t\"github.com/shirou/gopsutil/v4/process\"\n)\n\ntype stats struct {\n\tPID statsPID `json:\"pid\"`\n\tOS  statsOS  `json:\"os\"`\n}\n\ntype statsPID struct {\n\tCPU   float64 `json:\"cpu\"`\n\tRAM   uint64  `json:\"ram\"`\n\tConns int     `json:\"conns\"`\n}\n\ntype statsOS struct {\n\tCPU      float64 `json:\"cpu\"`\n\tRAM      uint64  `json:\"ram\"`\n\tTotalRAM uint64  `json:\"total_ram\"`\n\tLoadAvg  float64 `json:\"load_avg\"`\n\tConns    int     `json:\"conns\"`\n}\n\nvar (\n\tmonitPIDCPU   atomic.Value\n\tmonitPIDRAM   atomic.Value\n\tmonitPIDConns atomic.Value\n\n\tmonitOSCPU      atomic.Value\n\tmonitOSRAM      atomic.Value\n\tmonitOSTotalRAM atomic.Value\n\tmonitOSLoadAvg  atomic.Value\n\tmonitOSConns    atomic.Value\n)\n\nvar (\n\tmutex sync.RWMutex\n\tonce  sync.Once\n\tdata  = &stats{}\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// Start routine to update statistics\n\tonce.Do(func() {\n\t\tp, _ := process.NewProcess(int32(os.Getpid())) //nolint:errcheck // TODO: Handle error\n\t\tnumcpu := runtime.NumCPU()\n\t\tupdateStatistics(p, numcpu)\n\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\ttime.Sleep(cfg.Refresh)\n\n\t\t\t\tupdateStatistics(p, numcpu)\n\t\t\t}\n\t\t}()\n\t})\n\n\t// Return new handler\n\t//nolint:errcheck // Ignore the type-assertion errors\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 fiber.ErrMethodNotAllowed\n\t\t}\n\t\tif c.Get(fiber.HeaderAccept) == fiber.MIMEApplicationJSON || cfg.APIOnly {\n\t\t\tmutex.Lock()\n\t\t\tdata.PID.CPU, _ = monitPIDCPU.Load().(float64)\n\t\t\tdata.PID.RAM, _ = monitPIDRAM.Load().(uint64)\n\t\t\tdata.PID.Conns, _ = monitPIDConns.Load().(int)\n\n\t\t\tdata.OS.CPU, _ = monitOSCPU.Load().(float64)\n\t\t\tdata.OS.RAM, _ = monitOSRAM.Load().(uint64)\n\t\t\tdata.OS.TotalRAM, _ = monitOSTotalRAM.Load().(uint64)\n\t\t\tdata.OS.LoadAvg, _ = monitOSLoadAvg.Load().(float64)\n\t\t\tdata.OS.Conns, _ = monitOSConns.Load().(int)\n\t\t\tmutex.Unlock()\n\t\t\treturn c.Status(fiber.StatusOK).JSON(data)\n\t\t}\n\t\tc.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)\n\t\treturn c.Status(fiber.StatusOK).SendString(cfg.index)\n\t}\n}\n\nfunc updateStatistics(p *process.Process, numcpu int) {\n\tpidCPU, err := p.Percent(0)\n\tif err == nil {\n\t\tmonitPIDCPU.Store(pidCPU / float64(numcpu))\n\t}\n\n\tif osCPU, err := cpu.Percent(0, false); err == nil && len(osCPU) > 0 {\n\t\tmonitOSCPU.Store(osCPU[0])\n\t}\n\n\tif pidRAM, err := p.MemoryInfo(); err == nil && pidRAM != nil {\n\t\tmonitPIDRAM.Store(pidRAM.RSS)\n\t}\n\n\tif osRAM, err := mem.VirtualMemory(); err == nil && osRAM != nil {\n\t\tmonitOSRAM.Store(osRAM.Used)\n\t\tmonitOSTotalRAM.Store(osRAM.Total)\n\t}\n\n\tif loadAvg, err := load.Avg(); err == nil && loadAvg != nil {\n\t\tmonitOSLoadAvg.Store(loadAvg.Load1)\n\t}\n\n\tpidConns, err := net.ConnectionsPid(\"tcp\", p.Pid)\n\tif err == nil {\n\t\tmonitPIDConns.Store(len(pidConns))\n\t}\n\n\tosConns, err := net.Connections(\"tcp\")\n\tif err == nil {\n\t\tmonitOSConns.Store(len(osConns))\n\t}\n}\n"
  },
  {
    "path": "v3/monitor/monitor_test.go",
    "content": "package monitor\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\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/assert\"\n\n\t\"github.com/valyala/fasthttp\"\n)\n\nfunc Test_Monitor_405(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Use(\"/\", New())\n\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodPost, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 405, resp.StatusCode)\n}\n\nfunc Test_Monitor_Html(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// defaults\n\tapp.Get(\"/\", New())\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tassert.Equal(t, fiber.MIMETextHTMLCharsetUTF8,\n\t\tresp.Header.Get(fiber.HeaderContentType))\n\tbuf, err := io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(\"<title>\"+defaultTitle+\"</title>\")))\n\ttimeoutLine := fmt.Sprintf(\"setTimeout(fetchJSON, %d)\",\n\t\tdefaultRefresh.Milliseconds()-timeoutDiff)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine)))\n\n\t// custom config\n\tconf := Config{Title: \"New \" + defaultTitle, Refresh: defaultRefresh + time.Second}\n\tapp.Get(\"/custom\", New(conf))\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/custom\", nil))\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tassert.Equal(t, fiber.MIMETextHTMLCharsetUTF8,\n\t\tresp.Header.Get(fiber.HeaderContentType))\n\tbuf, err = io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(\"<title>\"+conf.Title+\"</title>\")))\n\ttimeoutLine = fmt.Sprintf(\"setTimeout(fetchJSON, %d)\",\n\t\tconf.Refresh.Milliseconds()-timeoutDiff)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine)))\n}\n\nfunc Test_Monitor_Html_CustomCodes(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\t// defaults\n\tapp.Get(\"/\", New())\n\tresp, err := app.Test(httptest.NewRequest(fiber.MethodGet, \"/\", nil))\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tassert.Equal(t, fiber.MIMETextHTMLCharsetUTF8,\n\t\tresp.Header.Get(fiber.HeaderContentType))\n\tbuf, err := io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(\"<title>\"+defaultTitle+\"</title>\")))\n\ttimeoutLine := fmt.Sprintf(\"setTimeout(fetchJSON, %d)\",\n\t\tdefaultRefresh.Milliseconds()-timeoutDiff)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine)))\n\n\t// custom config\n\tconf := Config{\n\t\tTitle:      \"New \" + defaultTitle,\n\t\tRefresh:    defaultRefresh + time.Second,\n\t\tChartJSURL: \"https://cdnjs.com/libraries/Chart.js\",\n\t\tFontURL:    \"/public/my-font.css\",\n\t\tCustomHead: `<style>body{background:#fff}</style>`,\n\t}\n\tapp.Get(\"/custom\", New(conf))\n\tresp, err = app.Test(httptest.NewRequest(fiber.MethodGet, \"/custom\", nil))\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tassert.Equal(t, fiber.MIMETextHTMLCharsetUTF8,\n\t\tresp.Header.Get(fiber.HeaderContentType))\n\tbuf, err = io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(\"<title>\"+conf.Title+\"</title>\")))\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(\"https://cdnjs.com/libraries/Chart.js\")))\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(\"/public/my-font.css\")))\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(conf.CustomHead)))\n\n\ttimeoutLine = fmt.Sprintf(\"setTimeout(fetchJSON, %d)\",\n\t\tconf.Refresh.Milliseconds()-timeoutDiff)\n\tassert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine)))\n}\n\n// go test -run Test_Monitor_JSON -race\nfunc Test_Monitor_JSON(t *testing.T) {\n\tt.Parallel()\n\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", New())\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", nil)\n\treq.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON)\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tassert.Equal(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tb, err := io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, bytes.Contains(b, []byte(\"pid\")))\n\tassert.Equal(t, true, bytes.Contains(b, []byte(\"os\")))\n}\n\n// go test -v -run=^$ -bench=Benchmark_Monitor -benchmem -count=4\nfunc Benchmark_Monitor(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", New())\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.HeaderAccept, fiber.MIMEApplicationJSON)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tb.RunParallel(func(pb *testing.PB) {\n\t\tfor pb.Next() {\n\t\t\th(fctx)\n\t\t}\n\t})\n\n\tassert.Equal(b, 200, fctx.Response.Header.StatusCode())\n\tassert.Equal(b,\n\t\tfiber.MIMEApplicationJSONCharsetUTF8,\n\t\tstring(fctx.Response.Header.Peek(fiber.HeaderContentType)))\n}\n\n// go test -run Test_Monitor_Next\nfunc Test_Monitor_Next(t *testing.T) {\n\tt.Parallel()\n\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.MethodPost, \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 404, resp.StatusCode)\n}\n\n// go test -run Test_Monitor_APIOnly -race\nfunc Test_Monitor_APIOnly(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Get(\"/\", New(Config{\n\t\tAPIOnly: true,\n\t}))\n\n\treq := httptest.NewRequest(fiber.MethodGet, \"/\", nil)\n\treq.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON)\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, 200, resp.StatusCode)\n\tassert.Equal(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType))\n\n\tb, err := io.ReadAll(resp.Body)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, bytes.Contains(b, []byte(\"pid\")))\n\tassert.Equal(t, true, bytes.Contains(b, []byte(\"os\")))\n}\n"
  },
  {
    "path": "v3/newrelic/README.md",
    "content": "---\nid: newrelic\n---\n\n# New Relic\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*newrelic*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20newrelic/badge.svg)\n\n[New Relic](https://github.com/newrelic/go-agent) support for Fiber.\n\nIncoming request headers are forwarded to New Relic transactions by default. This enables distributed tracing header processing, but can also forward sensitive headers. Use `RequestHeaderFilter` to allowlist or redact headers as needed.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/newrelic\n```\n\n## Signature\n\n```go\nmiddleware.New(config middleware.Config) fiber.Handler\nmiddleware.FromContext(ctx any) *nr.Transaction     // nr \"github.com/newrelic/go-agent/v3/newrelic\"\n```\n\n`FromContext` accepts a `fiber.Ctx`, `fiber.CustomCtx`, `*fasthttp.RequestCtx`, or a standard `context.Context` (e.g. the value returned by `c.Context()` when `PassLocalsToContext` is enabled). It returns an `*nr.Transaction` (a New Relic transaction from `github.com/newrelic/go-agent/v3/newrelic`).\n\n## Config\n\n| Property               | Type             | Description                                                 | Default                         |\n|:-----------------------|:-----------------|:------------------------------------------------------------|:--------------------------------|\n| License                | `string`         | Required - New Relic License Key                            | `\"\"`                            |\n| AppName                | `string`         | New Relic Application Name                                  | `fiber-api`                     |\n| Enabled                | `bool`           | Enable/Disable New Relic                                    | `false`                         |\n| ~~TransportType~~      | ~~`string`~~     | ~~Can be HTTP or HTTPS~~ (Deprecated)                       | ~~`\"HTTP\"`~~                    |\n| Application            | `Application`    | Existing New Relic App                                      | `nil`                           |\n| ErrorStatusCodeHandler | `func(c fiber.Ctx, err error) int`    | If you want to change newrelic status code, you can use it. | `DefaultErrorStatusCodeHandler` |\n| Next                   | `func(c fiber.Ctx) bool`    | Next defines a function to skip this middleware when returned true.                                                           | `nil`                           |\n| RequestHeaderFilter    | `func(key, value string) bool`    | Return `true` to forward a request header to New Relic, `false` to skip it. | `nil` (forward all headers) |\n\n## Usage\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    middleware \"github.com/gofiber/contrib/v3/newrelic\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Get(\"/\", func(ctx fiber.Ctx) error {\n        return ctx.SendStatus(200)\n    })\n\n    cfg := middleware.Config{\n        License:       \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n        AppName:       \"MyCustomApi\",\n        Enabled:       true,\n    }\n\n    app.Use(middleware.New(cfg))\n\n    app.Listen(\":8080\")\n}\n```\n\n## Usage with existing New Relic application\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    middleware \"github.com/gofiber/contrib/v3/newrelic\"\n    nr \"github.com/newrelic/go-agent/v3/newrelic\"\n)\n\nfunc main() {\n    nrApp, err := nr.NewApplication(\n        nr.ConfigAppName(\"MyCustomApi\"),\n        nr.ConfigLicense(\"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\"),\n        nr.ConfigEnabled(true),\n    )\n\n    app := fiber.New()\n\n    app.Get(\"/\", func(ctx fiber.Ctx) error {\n        return ctx.SendStatus(200)\n    })\n    \n    app.Get(\"/foo\", func(ctx fiber.Ctx) error {\n        txn := middleware.FromContext(ctx)\n        segment := txn.StartSegment(\"foo segment\")\n        defer segment.End()\n        \n        // do foo \n\n        return nil\n    })\n\n    cfg := middleware.Config{\n        Application:       nrApp,\n    }\n\n    app.Use(middleware.New(cfg))\n\n    app.Listen(\":8080\")\n}\n```\n\n## Retrieving the transaction with PassLocalsToContext\n\nWhen `fiber.Config{PassLocalsToContext: true}` is set, the New Relic transaction stored by the middleware is also available in the underlying `context.Context`. Use `FromContext` with any of the supported context types:\n\n```go\n// From a fiber.Ctx (most common usage)\ntxn := middleware.FromContext(c)\n\n// From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled)\ntxn := middleware.FromContext(c.Context())\n```\n"
  },
  {
    "path": "v3/newrelic/fiber.go",
    "content": "package newrelic\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/gofiber/utils/v2\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/newrelic/go-agent/v3/newrelic\"\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\ttransactionKey contextKey = iota\n)\n\ntype Config struct {\n\t// License parameter is required to initialize newrelic application\n\tLicense string\n\t// AppName parameter passed to set app name, default is fiber-api\n\tAppName string\n\t// Enabled parameter passed to enable/disable newrelic\n\tEnabled bool\n\t// TransportType can be HTTP or HTTPS, default is HTTP\n\t// Deprecated: The Transport type now acquiring from request URL scheme internally\n\tTransportType string\n\t// Application field is required to use an existing newrelic application\n\tApplication *newrelic.Application\n\t// ErrorStatusCodeHandler is executed when an error is returned from handler\n\t// Optional. Default: DefaultErrorStatusCodeHandler\n\tErrorStatusCodeHandler func(c fiber.Ctx, err error) int\n\t// Next defines a function to skip this middleware when returned true.\n\t// Optional. Default: nil\n\tNext func(c fiber.Ctx) bool\n\t// RequestHeaderFilter controls which inbound request headers are forwarded to\n\t// New Relic via WebRequest.Header.\n\t// Return true to include a header, false to exclude it.\n\t// Optional. Default: include all headers.\n\tRequestHeaderFilter func(key, value string) bool\n}\n\nvar ConfigDefault = Config{\n\tApplication:            nil,\n\tLicense:                \"\",\n\tAppName:                \"fiber-api\",\n\tEnabled:                false,\n\tErrorStatusCodeHandler: DefaultErrorStatusCodeHandler,\n\tNext:                   nil,\n\tRequestHeaderFilter:    nil,\n}\n\nfunc New(cfg Config) fiber.Handler {\n\tvar app *newrelic.Application\n\tvar err error\n\n\tif cfg.ErrorStatusCodeHandler == nil {\n\t\tcfg.ErrorStatusCodeHandler = ConfigDefault.ErrorStatusCodeHandler\n\t}\n\n\tif cfg.Application != nil {\n\t\tapp = cfg.Application\n\t} else {\n\t\tif cfg.AppName == \"\" {\n\t\t\tcfg.AppName = ConfigDefault.AppName\n\t\t}\n\n\t\tif cfg.License == \"\" {\n\t\t\tpanic(fmt.Errorf(\"unable to create New Relic Application -> License can not be empty\"))\n\t\t}\n\n\t\tapp, err = newrelic.NewApplication(\n\t\t\tnewrelic.ConfigAppName(cfg.AppName),\n\t\t\tnewrelic.ConfigLicense(cfg.License),\n\t\t\tnewrelic.ConfigEnabled(cfg.Enabled),\n\t\t)\n\n\t\tif err != nil {\n\t\t\tpanic(fmt.Errorf(\"unable to create New Relic Application -> %w\", err))\n\t\t}\n\t}\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\ttxn := app.StartTransaction(createTransactionName(c))\n\t\tdefer txn.End()\n\n\t\tvar (\n\t\t\thost   = utils.CopyString(c.Hostname())\n\t\t\tmethod = utils.CopyString(c.Method())\n\t\t)\n\n\t\tscheme := c.Request().URI().Scheme()\n\t\ttxn.SetWebRequest(createWebRequest(c, host, method, string(scheme), cfg.RequestHeaderFilter))\n\n\t\tfiber.StoreInContext(c, transactionKey, txn)\n\t\tc.SetContext(newrelic.NewContext(c.Context(), txn))\n\n\t\thandlerErr := c.Next()\n\t\tstatusCode := c.RequestCtx().Response.StatusCode()\n\n\t\tif handlerErr != nil {\n\t\t\tstatusCode = cfg.ErrorStatusCodeHandler(c, handlerErr)\n\t\t\ttxn.NoticeError(handlerErr)\n\t\t}\n\n\t\ttxn.SetWebResponse(nil).WriteHeader(statusCode)\n\n\t\treturn handlerErr\n\t}\n}\n\n// FromContext returns the Transaction from the context if present, and nil otherwise.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\nfunc FromContext(ctx any) *newrelic.Transaction {\n\tif txn, ok := fiber.ValueFromContext[*newrelic.Transaction](ctx, transactionKey); ok {\n\t\treturn txn\n\t}\n\n\tif ctx, ok := ctx.(context.Context); ok {\n\t\treturn newrelic.FromContext(ctx)\n\t}\n\n\treturn nil\n}\n\nfunc createTransactionName(c fiber.Ctx) string {\n\treturn fmt.Sprintf(\"%s %s\", c.Request().Header.Method(), c.Request().URI().Path())\n}\n\nfunc createWebRequest(c fiber.Ctx, host, method, scheme string, filter func(key, value string) bool) newrelic.WebRequest {\n\theaders := make(http.Header, c.Request().Header.Len())\n\tfor key, value := range c.Request().Header.All() {\n\t\theaderKey := string(key)\n\t\theaderValue := string(value)\n\n\t\tif filter != nil && !filter(headerKey, headerValue) {\n\t\t\tcontinue\n\t\t}\n\n\t\theaders.Add(headerKey, headerValue)\n\t}\n\n\treturn newrelic.WebRequest{\n\t\tHeader:    headers,\n\t\tHost:      host,\n\t\tMethod:    method,\n\t\tTransport: transport(scheme),\n\t\tURL: &url.URL{\n\t\t\tHost:     host,\n\t\t\tScheme:   scheme,\n\t\t\tPath:     string(c.Request().URI().Path()),\n\t\t\tRawQuery: string(c.Request().URI().QueryString()),\n\t\t},\n\t}\n}\n\nfunc transport(schema string) newrelic.TransportType {\n\tif strings.HasPrefix(schema, \"https\") {\n\t\treturn newrelic.TransportHTTPS\n\t}\n\n\tif strings.HasPrefix(schema, \"http\") {\n\t\treturn newrelic.TransportHTTP\n\t}\n\n\treturn newrelic.TransportUnknown\n}\n\nfunc DefaultErrorStatusCodeHandler(c fiber.Ctx, err error) int {\n\tif fiberErr, ok := err.(*fiber.Error); ok {\n\t\treturn fiberErr.Code\n\t}\n\n\treturn c.RequestCtx().Response.StatusCode()\n}\n"
  },
  {
    "path": "v3/newrelic/fiber_test.go",
    "content": "package newrelic\n\nimport (\n\t\"errors\"\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/newrelic/go-agent/v3/newrelic\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewRelicAppConfig(t *testing.T) {\n\tt.Run(\"Panic occurs when License empty\",\n\t\tfunc(t *testing.T) {\n\t\t\tassert.Panics(t, func() {\n\t\t\t\tNew(Config{\n\t\t\t\t\tLicense: \"\",\n\t\t\t\t\tAppName: \"\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\tt.Run(\"Run without panic when License not empty\",\n\t\tfunc(t *testing.T) {\n\t\t\tassert.NotPanics(t, func() {\n\t\t\t\tNew(Config{\n\t\t\t\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\t\t\t\tAppName: \"\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\tt.Run(\"Panic when License is invalid length\",\n\t\tfunc(t *testing.T) {\n\t\t\tassert.Panics(t, func() {\n\t\t\t\tNew(Config{\n\t\t\t\t\tLicense: \"invalid_key\",\n\t\t\t\t\tAppName: \"\",\n\t\t\t\t\tEnabled: false,\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\tt.Run(\"Run successfully as middleware\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t}\n\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Run successfully as middleware\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t}\n\n\t\t\tnewRelicApp, _ := newrelic.NewApplication(\n\t\t\t\tnewrelic.ConfigAppName(cfg.AppName),\n\t\t\t\tnewrelic.ConfigLicense(cfg.License),\n\t\t\t\tnewrelic.ConfigEnabled(cfg.Enabled),\n\t\t\t)\n\n\t\t\tcfg.Application = newRelicApp\n\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Test for invalid URL\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t}\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/invalid-url\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 404, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Test HTTP transport type\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t}\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Test http transport type (lowercase)\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t}\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Test HTTPS transport type\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t}\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Test using existing newrelic application (configured)\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tnewrelicApp, err := newrelic.NewApplication(\n\t\t\t\tnewrelic.ConfigAppName(\"testApp\"),\n\t\t\t\tnewrelic.ConfigLicense(\"0123456789abcdef0123456789abcdef01234567\"),\n\t\t\t\tnewrelic.ConfigEnabled(true),\n\t\t\t)\n\n\t\t\tcfg := Config{\n\t\t\t\tApplication: newrelicApp,\n\t\t\t}\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, newrelicApp)\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Assert panic with existing newrelic application (no config)\",\n\t\tfunc(t *testing.T) {\n\t\t\tassert.Panics(t, func() {\n\t\t\t\tapp := fiber.New()\n\n\t\t\t\tnewrelicApp, err := newrelic.NewApplication()\n\n\t\t\t\tcfg := Config{\n\t\t\t\t\tApplication: newrelicApp,\n\t\t\t\t}\n\t\t\t\tapp.Use(New(cfg))\n\n\t\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t\t})\n\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, newrelicApp)\n\n\t\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t\t})\n\t\t})\n\n\tt.Run(\"config should use default error status code handler\", func(t *testing.T) {\n\t\t// given\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tLicense: \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\tAppName: \"\",\n\t\t\tEnabled: true,\n\t\t}))\n\n\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error { return errors.New(\"system error\") })\n\n\t\t// when\n\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\tr.Host = \"localhost\"\n\t\tresp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\t})\n\n\tt.Run(\"config should use custom error status code handler when error status code handler is provided\", func(t *testing.T) {\n\t\t// given\n\t\tvar (\n\t\t\tapp                          = fiber.New()\n\t\t\terrorStatusCodeHandlerCalled = false\n\t\t)\n\n\t\terrorStatusCodeHandler := func(c fiber.Ctx, err error) int {\n\t\t\terrorStatusCodeHandlerCalled = true\n\t\t\treturn http.StatusInternalServerError\n\t\t}\n\n\t\tapp.Use(New(Config{\n\t\t\tLicense:                \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\tAppName:                \"\",\n\t\t\tEnabled:                true,\n\t\t\tErrorStatusCodeHandler: errorStatusCodeHandler,\n\t\t}))\n\n\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error { return errors.New(\"system error\") })\n\n\t\t// when\n\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\tr.Host = \"localhost\"\n\t\tresp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\t\tassert.True(t, errorStatusCodeHandlerCalled)\n\t})\n\n\tt.Run(\"Skip New Relic execution if next function is set\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t\tNext: func(c fiber.Ctx) bool {\n\t\t\t\t\treturn c.OriginalURL() == \"/jump\"\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tnewRelicApp, _ := newrelic.NewApplication(\n\t\t\t\tnewrelic.ConfigAppName(cfg.AppName),\n\t\t\t\tnewrelic.ConfigLicense(cfg.License),\n\t\t\t\tnewrelic.ConfigEnabled(cfg.Enabled),\n\t\t\t)\n\n\t\t\tcfg.Application = newRelicApp\n\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/jump\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/jump\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n\n\tt.Run(\"Continue New Relic execution if next function is set\",\n\t\tfunc(t *testing.T) {\n\t\t\tapp := fiber.New()\n\n\t\t\tcfg := Config{\n\t\t\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\t\t\tAppName: \"\",\n\t\t\t\tEnabled: true,\n\t\t\t\tNext: func(c fiber.Ctx) bool {\n\t\t\t\t\treturn c.OriginalURL() == \"/jump\"\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tnewRelicApp, _ := newrelic.NewApplication(\n\t\t\t\tnewrelic.ConfigAppName(cfg.AppName),\n\t\t\t\tnewrelic.ConfigLicense(cfg.License),\n\t\t\t\tnewrelic.ConfigEnabled(cfg.Enabled),\n\t\t\t)\n\n\t\t\tcfg.Application = newRelicApp\n\n\t\t\tapp.Use(New(cfg))\n\n\t\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\t\treturn ctx.SendStatus(200)\n\t\t\t})\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\t\tr.Host = \"localhost\"\n\t\t\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\t\tassert.Equal(t, 200, resp.StatusCode)\n\t\t})\n}\n\nfunc TestDefaultErrorStatusCodeHandler(t *testing.T) {\n\tt.Run(\"should return fiber status code when error is fiber error\", func(t *testing.T) {\n\t\t// given\n\t\terr := &fiber.Error{\n\t\t\tCode: http.StatusNotFound,\n\t\t}\n\n\t\t// when\n\t\tstatusCode := DefaultErrorStatusCodeHandler(nil, err)\n\n\t\t// then\n\t\tassert.Equal(t, http.StatusNotFound, statusCode)\n\t})\n\n\tt.Run(\"should return context status code when error is not fiber error\", func(t *testing.T) {\n\t\t// given\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tLicense: \"0123456789abcdef0123456789abcdef01234567\",\n\t\t\tAppName: \"\",\n\t\t\tEnabled: true,\n\t\t}))\n\n\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\terr := ctx.SendStatus(http.StatusNotFound)\n\t\t\tassert.Equal(t, http.StatusNotFound, DefaultErrorStatusCodeHandler(ctx, err))\n\t\t\treturn err\n\t\t})\n\n\t\t// when\n\t\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\t\tr.Host = \"localhost\"\n\t\tresp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusNotFound, resp.StatusCode)\n\t})\n}\n\nfunc TestFromContext(t *testing.T) {\n\t// given\n\tcfg := Config{\n\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\tAppName: \"\",\n\t\tEnabled: true,\n\t}\n\tapp := fiber.New()\n\tapp.Use(New(cfg))\n\tapp.Get(\"/foo\", func(ctx fiber.Ctx) error {\n\t\ttx := FromContext(ctx)\n\t\tassert.NotNil(t, tx)\n\n\t\tif tx != nil {\n\t\t\tsegment := tx.StartSegment(\"foo\")\n\t\t\tdefer segment.End()\n\t\t}\n\n\t\treturn ctx.SendStatus(http.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/foo\", http.NoBody)\n\treq.Host = \"localhost\"\n\n\t// when\n\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\t// then\n\tassert.Nil(t, err)\n\tassert.Equal(t, http.StatusOK, res.StatusCode)\n}\n\nfunc TestCreateWebRequest(t *testing.T) {\n\tt.Run(\"should include inbound headers for distributed tracing\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\treq := createWebRequest(ctx, ctx.Hostname(), ctx.Method(), string(ctx.Request().URI().Scheme()), nil)\n\t\t\tassert.Equal(t, \"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\", req.Header.Get(\"traceparent\"))\n\t\t\tassert.ElementsMatch(t, []string{\"abc\", \"def\"}, req.Header.Values(\"X-Custom\"))\n\t\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t\t})\n\n\t\tr := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\tr.Host = \"example.com\"\n\t\tr.Header.Set(\"traceparent\", \"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\")\n\t\tr.Header.Add(\"X-Custom\", \"abc\")\n\t\tr.Header.Add(\"X-Custom\", \"def\")\n\n\t\tresp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusNoContent, resp.StatusCode)\n\t})\n\n\tt.Run(\"should apply request header filter when configured\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\t\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\t\treq := createWebRequest(ctx, ctx.Hostname(), ctx.Method(), string(ctx.Request().URI().Scheme()), func(key, _ string) bool {\n\t\t\t\treturn strings.EqualFold(key, \"traceparent\")\n\t\t\t})\n\t\t\tassert.Equal(t, \"trace-value\", req.Header.Get(\"traceparent\"))\n\t\t\tassert.Empty(t, req.Header.Values(\"Authorization\"))\n\t\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t\t})\n\n\t\tr := httptest.NewRequest(http.MethodGet, \"/\", nil)\n\t\tr.Host = \"example.com\"\n\t\tr.Header.Set(\"traceparent\", \"trace-value\")\n\t\tr.Header.Set(\"Authorization\", \"Bearer secret\")\n\n\t\tresp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusNoContent, resp.StatusCode)\n\t})\n}\n\nfunc TestFromContext_PassLocalsToContext(t *testing.T) {\n\tcfg := Config{\n\t\tLicense: \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\n\t\tEnabled: true,\n\t}\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(New(cfg))\n\tapp.Get(\"/foo\", func(ctx fiber.Ctx) error {\n\t\ttx := FromContext(ctx)\n\t\ttxFromContext := FromContext(ctx.Context())\n\t\tassert.NotNil(t, tx)\n\t\tassert.NotNil(t, txFromContext)\n\t\treturn ctx.SendStatus(http.StatusOK)\n\t})\n\n\treq := httptest.NewRequest(http.MethodGet, \"/foo\", http.NoBody)\n\treq.Host = \"localhost\"\n\tres, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\tassert.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, res.StatusCode)\n}\n"
  },
  {
    "path": "v3/newrelic/go.mod",
    "content": "module github.com/gofiber/contrib/v3/newrelic\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/newrelic/go-agent/v3 v3.43.2\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgo.opentelemetry.io/otel v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect\n\tgoogle.golang.org/grpc v1.80.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/newrelic/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/newrelic/go-agent/v3 v3.43.2 h1:I8M0Do/sPtbT0daCMrxc9G6GeST+eDgsIpRHFAFRdOg=\ngithub.com/newrelic/go-agent/v3 v3.43.2/go.mod h1:MFXnCId5xXMIJI6A/kbkg0DO48EVTsKcmNijMYphzTg=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=\ngo.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=\ngo.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=\ngo.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=\ngo.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=\ngo.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=\ngo.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=\ngo.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=\ngonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=\ngoogle.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=\ngoogle.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/opa/README.md",
    "content": "---\nid: opa\n---\n\n# OPA\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*opa*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20opa/badge.svg)\n\n[Open Policy Agent](https://github.com/open-policy-agent/opa) support for Fiber.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/opa\n```\n\n## Signature\n\n```go\nopa.New(config opa.Config) fiber.Handler\n\n```\n\n## Config\n\n| Property              | Type                | Description                                                  | Default                                                             |\n|:----------------------|:--------------------|:-------------------------------------------------------------|:--------------------------------------------------------------------|\n| RegoQuery             | `string`            | Required - Rego query                                        | -                                                                   |\n| RegoPolicy            | `io.Reader`         | Required - Rego policy                                       | -                                                                   |\n| IncludeQueryString    | `bool`              | Include query string as input to rego policy                 | `false`                                                             |\n| DeniedStatusCode      | `int`               | Http status code to return when policy denies request        | `400`                                                               |\n| DeniedResponseMessage | `string`            | Http response body text to return when policy denies request | `\"\"`                                                                |\n| IncludeHeaders        | `[]string`          | Include headers as input to rego policy                      | -                                                                   |\n| InputCreationMethod   | `InputCreationFunc` | Use your own function to provide input for OPA               | `func defaultInput(ctx fiber.Ctx) (map[string]interface{}, error)` |\n\n## Types\n\n```go\ntype InputCreationFunc func(c fiber.Ctx) (map[string]interface{}, error)\n```\n\n## Usage\n\nOPA Fiber middleware sends the following example data to the policy engine as input:\n\n```json\n{\n  \"method\": \"GET\",\n  \"path\": \"/somePath\",\n  \"query\": {\n    \"name\": [\"John Doe\"]\n  },\n  \"headers\": {\n    \"Accept\": \"application/json\",\n    \"Content-Type\": \"application/json\"\n  }\n}\n```\n\n```go\npackage main\n\nimport (\n    \"bytes\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/contrib/v3/opa\"\n)\n\nfunc main() {\n    app := fiber.New()\n    module := `\npackage example.authz\n\ndefault allow := false\n\nallow if {\n    input.method == \"GET\"\n}\n`\n\n    cfg := opa.Config{\n        RegoQuery:             \"data.example.authz.allow\",\n        RegoPolicy:            bytes.NewBufferString(module),\n        IncludeQueryString:    true,\n        DeniedStatusCode:      fiber.StatusForbidden,\n        DeniedResponseMessage: \"status forbidden\",\n        IncludeHeaders:        []string{\"Authorization\"},\n        InputCreationMethod:   func(ctx fiber.Ctx) (map[string]interface{}, error) {\n            return map[string]interface{}{\n                \"method\": ctx.Method(),\n                \"path\": ctx.Path(),\n            }, nil\n        },\n    }\n    app.Use(opa.New(cfg))\n\n    app.Get(\"/\", func(ctx fiber.Ctx) error {\n        return ctx.SendStatus(200)\n    })\n\n    app.Listen(\":8080\")\n}\n```\n"
  },
  {
    "path": "v3/opa/fiber.go",
    "content": "package opa\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/open-policy-agent/opa/v1/rego\"\n)\n\ntype InputCreationFunc func(c fiber.Ctx) (map[string]interface{}, error)\n\ntype Config struct {\n\tRegoPolicy            io.Reader\n\tRegoQuery             string\n\tIncludeHeaders        []string\n\tIncludeQueryString    bool\n\tDeniedStatusCode      int\n\tDeniedResponseMessage string\n\tInputCreationMethod   InputCreationFunc\n}\n\nfunc New(cfg Config) fiber.Handler {\n\terr := cfg.fillAndValidate()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treadedBytes, err := io.ReadAll(cfg.RegoPolicy)\n\tif err != nil {\n\t\tpanic(fmt.Sprint(\"could not read rego policy %w\", err))\n\t}\n\tquery, err := rego.New(\n\t\trego.Query(cfg.RegoQuery),\n\t\trego.Module(\"policy.rego\", utils.UnsafeString(readedBytes)),\n\t).PrepareForEval(context.Background())\n\tif err != nil {\n\t\tpanic(fmt.Sprint(\"rego policy error: %w\", err))\n\t}\n\treturn func(c fiber.Ctx) error {\n\t\tinput, err := cfg.InputCreationMethod(c)\n\t\tif err != nil {\n\t\t\tc.Response().SetStatusCode(fiber.StatusInternalServerError)\n\t\t\tc.Response().SetBodyString(fmt.Sprintf(\"Error creating input: %s\", err))\n\t\t\treturn err\n\t\t}\n\t\tif cfg.IncludeQueryString {\n\t\t\tqueryStringData := make(map[string][]string)\n\t\t\tfor key, value := range c.Request().URI().QueryArgs().All() {\n\t\t\t\tk := utils.UnsafeString(key)\n\t\t\t\tqueryStringData[k] = append(queryStringData[k], utils.UnsafeString(value))\n\t\t\t}\n\t\t\tinput[\"query\"] = queryStringData\n\t\t}\n\t\tif len(cfg.IncludeHeaders) > 0 {\n\t\t\theaders := make(map[string]string)\n\t\t\tfor _, header := range cfg.IncludeHeaders {\n\t\t\t\theaders[header] = c.Get(header)\n\t\t\t}\n\t\t\tinput[\"headers\"] = headers\n\t\t}\n\t\tres, err := query.Eval(context.Background(), rego.EvalInput(input))\n\t\tif err != nil {\n\t\t\tc.Response().SetStatusCode(fiber.StatusInternalServerError)\n\t\t\tc.Response().SetBodyString(fmt.Sprintf(\"Error evaluating rego policy: %s\", err))\n\t\t\treturn err\n\t\t}\n\n\t\tif !res.Allowed() {\n\t\t\tc.Response().SetStatusCode(cfg.DeniedStatusCode)\n\t\t\tc.Response().SetBodyString(cfg.DeniedResponseMessage)\n\t\t\treturn nil\n\t\t}\n\n\t\treturn c.Next()\n\t}\n}\n\nfunc (c *Config) fillAndValidate() error {\n\tif c.RegoQuery == \"\" {\n\t\treturn fmt.Errorf(\"rego query can not be empty\")\n\t}\n\n\tif c.DeniedStatusCode == 0 {\n\t\tc.DeniedStatusCode = fiber.StatusBadRequest\n\t}\n\tif c.DeniedResponseMessage == \"\" {\n\t\tc.DeniedResponseMessage = fiber.ErrBadRequest.Error()\n\t}\n\tif c.IncludeHeaders == nil {\n\t\tc.IncludeHeaders = []string{}\n\t}\n\tif c.InputCreationMethod == nil {\n\t\tc.InputCreationMethod = defaultInput\n\t}\n\treturn nil\n}\n\nfunc defaultInput(ctx fiber.Ctx) (map[string]interface{}, error) {\n\tinput := map[string]interface{}{\n\t\t\"method\": ctx.Method(),\n\t\t\"path\":   ctx.Path(),\n\t}\n\treturn input, nil\n}\n"
  },
  {
    "path": "v3/opa/fiber_test.go",
    "content": "package opa\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPanicWhenRegoQueryEmpty(t *testing.T) {\n\tapp := fiber.New()\n\n\tassert.Panics(t, func() {\n\t\tapp.Use(New(Config{}))\n\t})\n}\n\nfunc TestDefaultDeniedStatusCode400WhenConfigDeniedStatusCodeEmpty(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\nimport future.keywords\n\ndefault allow := false\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tDeniedResponseMessage: \"not allowed\",\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, 400, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"not allowed\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaNotAllowedRegoPolicyShouldReturnConfigDeniedStatusCode(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\nimport future.keywords\n\ndefault allow := false\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tDeniedStatusCode:      401,\n\t\tDeniedResponseMessage: \"not allowed\",\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, 401, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"not allowed\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaRequestMethodRegoPolicyShouldReturnConfigDeniedStatusCode(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\ndefault allow := false\n\nallow if {\n        input.method == \"GET\"\n}\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tDeniedStatusCode:      fiber.StatusMethodNotAllowed,\n\t\tDeniedResponseMessage: \"method not allowed\",\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"POST\", \"/\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, fiber.StatusMethodNotAllowed, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"method not allowed\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaRequestPathRegoPolicyShouldReturnOK(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\ndefault allow := false\n\nallow if {\n        input.path == \"/path\"\n}\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tDeniedStatusCode:      fiber.StatusOK,\n\t\tDeniedResponseMessage: \"OK\",\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Post(\"/path\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"POST\", \"/path\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"OK\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaQueryStringRegoPolicyShouldReturnOK(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\nimport future.keywords.in\n\ndefault allow := false\n\nallow if {\n        input.query == {\"testKey\": [\"testVal\"]}\n}\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tIncludeQueryString:    true,\n\t\tDeniedStatusCode:      fiber.StatusBadRequest,\n\t\tDeniedResponseMessage: \"bad request\",\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/test\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/test?testKey=testVal\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"OK\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaRequestHeadersRegoPolicyShouldReturnOK(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\nimport future.keywords.in\n\ndefault allow := false\n\nallow if {\n        input.headers == {\"testHeaderKey\": \"testHeaderVal\"}\n}\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tIncludeQueryString:    true,\n\t\tDeniedStatusCode:      fiber.StatusBadRequest,\n\t\tDeniedResponseMessage: \"bad request\",\n\t\tIncludeHeaders:        []string{\"testHeaderKey\"},\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/headers\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/headers\", nil)\n\tr.Header.Set(\"testHeaderKey\", \"testHeaderVal\")\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"OK\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaRequestWithCustomInput(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\ndefault allow := false\n\nallow if {\n        input.custom == \"test\"\n}\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tIncludeQueryString:    true,\n\t\tDeniedStatusCode:      fiber.StatusBadRequest,\n\t\tDeniedResponseMessage: \"bad request\",\n\t\tInputCreationMethod: func(c fiber.Ctx) (map[string]interface{}, error) {\n\t\t\treturn map[string]interface{}{\n\t\t\t\t\"custom\": \"test\",\n\t\t\t}, nil\n\t\t},\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/headers\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/headers\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"OK\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestOpaRequestWithCustomInputError(t *testing.T) {\n\tapp := fiber.New()\n\tmodule := `\npackage example.authz\n\ndefault allow := false\n\nallow if {\n        input.custom == \"test\"\n}\n`\n\n\tcfg := Config{\n\t\tRegoQuery:             \"data.example.authz.allow\",\n\t\tRegoPolicy:            bytes.NewBufferString(module),\n\t\tIncludeQueryString:    true,\n\t\tDeniedStatusCode:      fiber.StatusBadRequest,\n\t\tDeniedResponseMessage: \"bad request\",\n\t\tInputCreationMethod: func(c fiber.Ctx) (map[string]interface{}, error) {\n\t\t\treturn nil, errors.New(\"test error\")\n\t\t},\n\t}\n\tapp.Use(New(cfg))\n\n\tapp.Get(\"/headers\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(200)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/headers\", nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\treadedBytes, err := io.ReadAll(resp.Body)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test error\", utils.UnsafeString(readedBytes))\n}\n\nfunc TestFillAndValidate(t *testing.T) {\n\tcfg := Config{}\n\terr := cfg.fillAndValidate()\n\tassert.Error(t, err)\n\tcfg = Config{\n\t\tRegoPolicy: bytes.NewBufferString(\"test\"),\n\t}\n\terr = cfg.fillAndValidate()\n\tassert.Error(t, err)\n\tcfg = Config{\n\t\tRegoPolicy: bytes.NewBufferString(\"test\"),\n\t\tRegoQuery:  \"test\",\n\t}\n\terr = cfg.fillAndValidate()\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, cfg.DeniedStatusCode, fiber.StatusBadRequest)\n\tassert.Equal(t, cfg.DeniedResponseMessage, fiber.ErrBadRequest.Error())\n\tassert.IsType(t, cfg.InputCreationMethod, InputCreationFunc(nil))\n\tassert.IsType(t, cfg.IncludeHeaders, []string(nil))\n}\n"
  },
  {
    "path": "v3/opa/go.mod",
    "content": "module github.com/gofiber/contrib/v3/opa\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/open-policy-agent/opa v1.15.2\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/agnivade/levenshtein v1.2.1 // indirect\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/goccy/go-json v0.10.6 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lestrrat-go/blackmagic v1.0.4 // indirect\n\tgithub.com/lestrrat-go/dsig v1.3.0 // indirect\n\tgithub.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect\n\tgithub.com/lestrrat-go/httpcc v1.0.1 // indirect\n\tgithub.com/lestrrat-go/httprc/v3 v3.0.5 // indirect\n\tgithub.com/lestrrat-go/jwx/v3 v3.1.0 // indirect\n\tgithub.com/lestrrat-go/option/v2 v2.0.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.20.1 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/segmentio/asm v1.2.1 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/tchap/go-patricia/v2 v2.3.3 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgithub.com/valyala/fastjson v1.6.10 // indirect\n\tgithub.com/vektah/gqlparser/v2 v2.5.32 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/yashtewari/glob-intersection v0.2.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.4 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tsigs.k8s.io/yaml v1.6.0 // indirect\n)\n"
  },
  {
    "path": "v3/opa/go.sum",
    "content": "github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=\ngithub.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE=\ngithub.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=\ngithub.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=\ngithub.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w=\ngithub.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=\ngithub.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=\ngithub.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=\ngithub.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=\ngithub.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=\ngithub.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=\ngithub.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=\ngithub.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=\ngithub.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=\ngithub.com/lestrrat-go/dsig v1.3.0 h1:phjMOCXvYzhuIgn7Voe2rex8z166vGfxRxmqM25P9/Q=\ngithub.com/lestrrat-go/dsig v1.3.0/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc=\ngithub.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=\ngithub.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=\ngithub.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=\ngithub.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=\ngithub.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=\ngithub.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=\ngithub.com/lestrrat-go/jwx/v3 v3.1.0 h1:AyyLtxc0QM75F75JroWgt1phwC7X+wOb3XKhH7XBZWw=\ngithub.com/lestrrat-go/jwx/v3 v3.1.0/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU=\ngithub.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=\ngithub.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=\ngithub.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/open-policy-agent/opa v1.15.2 h1:dS9q+0Yvruq/VNvWJc5qCvCchn715OWc3HLHXn/UCCc=\ngithub.com/open-policy-agent/opa v1.15.2/go.mod h1:c6SN+7jSsUcKJLQc5P4yhwx8YYDRbjpAiGkBOTqxaa4=\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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=\ngithub.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=\ngithub.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=\ngithub.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=\ngithub.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\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/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\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/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=\ngithub.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=\ngithub.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\ngithub.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=\ngithub.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=\ngithub.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=\ngithub.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=\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/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=\ngithub.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=\ngo.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\n"
  },
  {
    "path": "v3/otel/README.md",
    "content": "---\nid: otel\n---\n\n# OTel\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*otel*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20otel/badge.svg)\n\n[OpenTelemetry](https://opentelemetry.io/) support for Fiber.\n\nThis package is listed on the [OpenTelemetry Registry](https://opentelemetry.io/registry/instrumentation-go-fiber/).\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/contrib/v3/otel\n```\n\n## Signature\n\n```go\notel.Middleware(opts ...otel.Option) fiber.Handler\n```\n\n## Config\nYou can configure the middleware using functional parameters\n\n| Function                | Argument Type                            | Description                                                                      | Default                                                             |\n| :------------------------ | :-------------------------------- | :--------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |\n| `WithNext`                    | `func(fiber.Ctx) bool`         | Define a function to skip this middleware when returned true .| nil                                                                 |\n| `WithTracerProvider`          | `oteltrace.TracerProvider`      | Specifies a tracer provider to use for creating a tracer.                         | nil - the global tracer provider is used                                   |\n| `WithMeterProvider`           | `otelmetric.MeterProvider`      | Specifies a meter provider to use for reporting.                                     | nil - the global meter provider is used                                                             |\n| `WithPort`                    | `int`                          | Specifies the value to use when setting the `server.port` attribute on metrics/spans.                            | Defaults to (`80` for `http`, `443` for `https`)              |\n| `WithPropagators`             | `propagation.TextMapPropagator` | Specifies propagators to use for extracting information from the HTTP requests.                     | If none are specified, global ones will be used                                                               |\n| (❌ **Removed**) `WithServerName`             | `string`                       | This option was removed because the `http.server_name` attribute is deprecated in the OpenTelemetry semantic conventions. The recommended attribute is `server.address`, which this middleware already fills with the hostname reported by Fiber.                                            | -                                                                   |\n| `WithSpanNameFormatter`       | `func(fiber.Ctx) string`       | Takes a function that will be called on every request and the returned string will become the span Name.                                   | Default formatter returns the route pathRaw |\n| `WithCustomAttributes`        | `func(fiber.Ctx) []attribute.KeyValue` | Define a function to add custom attributes to the span.                  | nil                                                                 |\n| `WithCustomMetricAttributes`  | `func(fiber.Ctx) []attribute.KeyValue` | Define a function to add custom attributes to the metrics.               | nil                                                                 |\n| `WithClientIP`         | `bool` | Specifies whether to collect the client's IP address from the request. | true |\n| (⚠️ **Deprecated**) `WithCollectClientIP`         | `bool` | Deprecated alias for `WithClientIP`. | true |\n| `WithoutMetrics`         | `bool` | Disables metrics collection when set to true. | false |\n\n## Usage\n\nPlease refer to [example](./example)\n\n## Metrics Notes\n\n- `http.server.request.size` and `http.server.response.size` are measured without buffering full streamed bodies into memory.\n- For streamed responses, size is recorded when the stream reaches EOF.\n- For `text/event-stream` responses (SSE), response body size is not recorded.\n\n## Example\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"errors\"\n    \"log\"\n\n    \"go.opentelemetry.io/otel/sdk/resource\"\n\n    \"github.com/gofiber/fiber/v3\"\n\n    fiberotel \"github.com/gofiber/contrib/v3/otel\"\n    \"go.opentelemetry.io/otel\"\n    \"go.opentelemetry.io/otel/attribute\"\n    stdout \"go.opentelemetry.io/otel/exporters/stdout/stdouttrace\"\n\n    //\"go.opentelemetry.io/otel/exporters/jaeger\"\n    \"go.opentelemetry.io/otel/propagation\"\n    sdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n    semconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n    oteltrace \"go.opentelemetry.io/otel/trace\"\n)\n\nvar tracer = otel.Tracer(\"fiber-server\")\n\nfunc main() {\n    tp := initTracer()\n    defer func() {\n        if err := tp.Shutdown(context.Background()); err != nil {\n            log.Printf(\"Error shutting down tracer provider: %v\", err)\n        }\n    }()\n\n    app := fiber.New()\n\n    app.Use(fiberotel.Middleware())\n\n    app.Get(\"/error\", func(ctx fiber.Ctx) error {\n        return errors.New(\"abc\")\n    })\n\n    app.Get(\"/users/:id\", func(c fiber.Ctx) error {\n        id := c.Params(\"id\")\n        name := getUser(c.Context(), id)\n        return c.JSON(fiber.Map{\"id\": id, \"name\": name})\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n\nfunc initTracer() *sdktrace.TracerProvider {\n    exporter, err := stdout.New(stdout.WithPrettyPrint())\n    if err != nil {\n        log.Fatal(err)\n    }\n    tp := sdktrace.NewTracerProvider(\n        sdktrace.WithSampler(sdktrace.AlwaysSample()),\n        sdktrace.WithBatcher(exporter),\n        sdktrace.WithResource(\n            resource.NewWithAttributes(\n                semconv.SchemaURL,\n                semconv.ServiceNameKey.String(\"my-service\"),\n            )),\n    )\n    otel.SetTracerProvider(tp)\n    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))\n    return tp\n}\n\nfunc getUser(ctx context.Context, id string) string {\n    _, span := tracer.Start(ctx, \"getUser\", oteltrace.WithAttributes(attribute.String(\"id\", id)))\n    defer span.End()\n    if id == \"123\" {\n        return \"otel tester\"\n    }\n    return \"unknown\"\n}\n```\n"
  },
  {
    "path": "v3/otel/config.go",
    "content": "package otel\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\totelmetric \"go.opentelemetry.io/otel/metric\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\toteltrace \"go.opentelemetry.io/otel/trace\"\n)\n\n// config is used to configure the Fiber middleware.\ntype config struct {\n\tNext                   func(fiber.Ctx) bool\n\tTracerProvider         oteltrace.TracerProvider\n\tMeterProvider          otelmetric.MeterProvider\n\tPort                   *int\n\tPropagators            propagation.TextMapPropagator\n\tSpanNameFormatter      func(fiber.Ctx) string\n\tCustomAttributes       func(fiber.Ctx) []attribute.KeyValue\n\tCustomMetricAttributes func(fiber.Ctx) []attribute.KeyValue\n\tclientIP               bool\n\twithoutMetrics         bool\n}\n\n// Option specifies instrumentation configuration options.\ntype Option interface {\n\tapply(*config)\n}\n\ntype optionFunc func(*config)\n\nfunc (o optionFunc) apply(c *config) {\n\to(c)\n}\n\n// WithNext takes a function that will be called on every\n// request, the middleware will be skipped if returning true\nfunc WithNext(f func(ctx fiber.Ctx) bool) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.Next = f\n\t})\n}\n\n// WithPropagators specifies propagators to use for extracting\n// information from the HTTP requests. If none are specified, global\n// ones will be used.\nfunc WithPropagators(propagators propagation.TextMapPropagator) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.Propagators = propagators\n\t})\n}\n\n// WithTracerProvider specifies a tracer provider to use for creating a tracer.\n// If none is specified, the global provider is used.\nfunc WithTracerProvider(provider oteltrace.TracerProvider) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.TracerProvider = provider\n\t})\n}\n\n// WithMeterProvider specifies a meter provider to use for reporting.\n// If none is specified, the global provider is used.\nfunc WithMeterProvider(provider otelmetric.MeterProvider) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.MeterProvider = provider\n\t})\n}\n\n// WithSpanNameFormatter takes a function that will be called on every\n// request and the returned string will become the Span Name\nfunc WithSpanNameFormatter(f func(ctx fiber.Ctx) string) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.SpanNameFormatter = f\n\t})\n}\n\n// WithPort specifies the value to use when setting the `server.port`\n// attribute on metrics/spans. Attribute is \"Conditionally Required: If not\n// default (`80` for `http`, `443` for `https`).\nfunc WithPort(port int) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.Port = &port\n\t})\n}\n\n// WithCustomAttributes specifies a function that will be called on every\n// request and the returned attributes will be added to the span.\nfunc WithCustomAttributes(f func(ctx fiber.Ctx) []attribute.KeyValue) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.CustomAttributes = f\n\t})\n}\n\n// WithCustomMetricAttributes specifies a function that will be called on every\n// request and the returned attributes will be added to the metrics.\nfunc WithCustomMetricAttributes(f func(ctx fiber.Ctx) []attribute.KeyValue) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.CustomMetricAttributes = f\n\t})\n}\n\n// WithClientIP specifies whether to collect the client's IP address\n// from the request. This is enabled by default.\nfunc WithClientIP(collect bool) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.clientIP = collect\n\t})\n}\n\n// WithCollectClientIP is deprecated and kept for backwards compatibility.\n// Deprecated: use WithClientIP instead.\nfunc WithCollectClientIP(collect bool) Option {\n\treturn WithClientIP(collect)\n}\n\n// WithoutMetrics disables metrics collection when set to true\nfunc WithoutMetrics(withoutMetrics bool) Option {\n\treturn optionFunc(func(cfg *config) {\n\t\tcfg.withoutMetrics = withoutMetrics\n\t})\n}\n"
  },
  {
    "path": "v3/otel/doc.go",
    "content": "// Package otel instruments the github.com/gofiber/fiber package.\n// (https://github.com/gofiber/fiber).\n//\n// Currently, only the routing of a received message can be instrumented. To do\n// so, use the Middleware function.\npackage otel // import \"github.com/gofiber/contrib/v3/otel\"\n"
  },
  {
    "path": "v3/otel/example/Dockerfile",
    "content": "FROM golang:alpine AS base\nCOPY . /src/\nWORKDIR /src/instrumentation/github.com/gofiber/fiber/otelefiber/example\n\nFROM base AS fiber-server\nRUN go install ./server.go\nCMD [\"/go/bin/server\"]\n"
  },
  {
    "path": "v3/otel/example/README.md",
    "content": "---\nid: otel-example\n---\n\n# Example\n\nAn HTTP server using gofiber fiber and instrumentation. The server has a\n`/users/:id` endpoint. The server generates span information to\n`stdout`.\n\nThese instructions expect you have\n[docker-compose](https://docs.docker.com/compose/) installed.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\nBring up the `fiber-server` and `fiber-client` services to run the\nexample:\n\n```sh\ndocker-compose up --detach fiber-server fiber-client\n```\n\nThe `fiber-client` service sends just one HTTP request to `fiber-server`\nand then exits. View the span generated by `fiber-server` in the logs:\n\n```sh\ndocker-compose logs fiber-server\n```\n\nShut down the services when you are finished with the example:\n\n```sh\ndocker-compose down\n```\n\n"
  },
  {
    "path": "v3/otel/example/docker-compose.yml",
    "content": "version: \"3.7\"\nservices:\n  fiber-client:\n    image: golang:alpine\n    networks:\n      - example\n    command:\n      - \"/bin/sh\"\n      - \"-c\"\n      - \"wget http://fiber-server:3000/users/123 && cat 123\"\n    depends_on:\n      - fiber-server\n  fiber-server:\n    build:\n      dockerfile: $PWD/Dockerfile\n    ports:\n      - \"3000:80\"\n    command:\n      - \"/bin/sh\"\n      - \"-c\"\n      - \"/go/bin/server\"\n    networks:\n      - example\nnetworks:\n  example:\n"
  },
  {
    "path": "v3/otel/example/go.mod",
    "content": "module github.com/gofiber/contrib/v3/otel/example\n\ngo 1.25.0\n\nreplace github.com/gofiber/contrib/v3/otel => ../\n\nrequire (\n\tgithub.com/gofiber/contrib/v3/otel v1.0.0\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgo.opentelemetry.io/otel v1.43.0\n\tgo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0\n\tgo.opentelemetry.io/otel/sdk v1.43.0\n\tgo.opentelemetry.io/otel/trace v1.43.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.4 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.43.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n)\n"
  },
  {
    "path": "v3/otel/example/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.4 h1:WwAxUA7L4MW2DjdEHF234lfqvBqd2vYYuBtA9TJq2ec=\ngithub.com/gofiber/utils/v2 v2.0.4/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib v1.43.0 h1:rv+pngknCr4qpZDxSpEvEoRioutgfbkk82x6MChJQ3U=\ngo.opentelemetry.io/contrib v1.43.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk=\ngo.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=\ngo.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=\ngo.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=\ngo.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=\ngo.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=\ngo.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=\ngo.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=\ngo.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\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": "v3/otel/example/server.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log\"\n\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\n\t\"github.com/gofiber/contrib/v3/otel\"\n\totelApi \"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\tstdout \"go.opentelemetry.io/otel/exporters/stdout/stdouttrace\"\n\n\t//\"go.opentelemetry.io/otel/exporters/jaeger\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.4.0\"\n\toteltrace \"go.opentelemetry.io/otel/trace\"\n)\n\nvar tracer = otelApi.Tracer(\"fiber-server\")\n\nfunc main() {\n\ttp := initTracer()\n\tdefer func() {\n\t\tif err := tp.Shutdown(context.Background()); err != nil {\n\t\t\tlog.Printf(\"Error shutting down tracer provider: %v\", err)\n\t\t}\n\t}()\n\n\tapp := fiber.New()\n\n\t// customise span name\n\t//app.Use(otel.Middleware(otel.WithSpanNameFormatter(func(ctx fiber.Ctx) string {\n\t//\treturn fmt.Sprintf(\"%s - %s\", ctx.Method(), ctx.Route().Path)\n\t//})))\n\n\tapp.Use(otel.Middleware())\n\n\tapp.Get(\"/error\", func(ctx fiber.Ctx) error {\n\t\treturn errors.New(\"abc\")\n\t})\n\n\tapp.Get(\"/users/:id\", func(c fiber.Ctx) error {\n\t\tid := c.Params(\"id\")\n\t\tname := getUser(c, id)\n\t\treturn c.JSON(fiber.Map{\"id\": id, name: name})\n\t})\n\n\tlog.Fatal(app.Listen(\":3000\"))\n}\n\nfunc initTracer() *sdktrace.TracerProvider {\n\texporter, err := stdout.New(stdout.WithPrettyPrint())\n\t//exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(\"http://localhost:14268/api/traces\")))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\ttp := sdktrace.NewTracerProvider(\n\t\tsdktrace.WithSampler(sdktrace.AlwaysSample()),\n\t\tsdktrace.WithBatcher(exporter),\n\t\tsdktrace.WithResource(\n\t\t\tresource.NewWithAttributes(\n\t\t\t\tsemconv.SchemaURL,\n\t\t\t\tsemconv.ServiceNameKey.String(\"my-service\"),\n\t\t\t)),\n\t)\n\totelApi.SetTracerProvider(tp)\n\totelApi.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))\n\treturn tp\n}\n\nfunc getUser(ctx context.Context, id string) string {\n\t_, span := tracer.Start(ctx, \"getUser\", oteltrace.WithAttributes(attribute.String(\"id\", id)))\n\tdefer span.End()\n\tif id == \"123\" {\n\t\treturn \"otel tester\"\n\t}\n\treturn \"unknown\"\n}\n"
  },
  {
    "path": "v3/otel/fiber.go",
    "content": "package otel\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gofiber/contrib/v3/otel/internal\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\totelcontrib \"go.opentelemetry.io/contrib\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/baggage\"\n\t\"go.opentelemetry.io/otel/metric\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n\toteltrace \"go.opentelemetry.io/otel/trace\"\n)\n\nconst (\n\ttracerKey           = \"gofiber-contrib-tracer-fiber\"\n\tinstrumentationName = \"github.com/gofiber/contrib/v3/otel\"\n\n\tMetricNameHTTPServerRequestDuration  = \"http.server.request.duration\"\n\tMetricNameHTTPServerRequestBodySize  = \"http.server.request.body.size\"\n\tMetricNameHTTPServerResponseBodySize = \"http.server.response.body.size\"\n\tMetricNameHTTPServerActiveRequests   = \"http.server.active_requests\"\n\n\t// Unit constants for deprecated metric units\n\tUnitDimensionless = \"1\"\n\tUnitBytes         = \"By\"\n\tUnitSeconds       = \"s\"\n\n\t// Deprecated: use MetricNameHTTPServerRequestDuration.\n\tMetricNameHttpServerDuration = MetricNameHTTPServerRequestDuration\n\t// Deprecated: use MetricNameHTTPServerRequestBodySize.\n\tMetricNameHttpServerRequestSize = MetricNameHTTPServerRequestBodySize\n\t// Deprecated: use MetricNameHTTPServerResponseBodySize.\n\tMetricNameHttpServerResponseSize = MetricNameHTTPServerResponseBodySize\n\t// Deprecated: use MetricNameHTTPServerActiveRequests.\n\tMetricNameHttpServerActiveRequests = MetricNameHTTPServerActiveRequests\n\t// Deprecated: kept for backward compatibility with legacy millisecond-based metrics.\n\t// New duration metrics use UnitSeconds.\n\tUnitMilliseconds = \"ms\"\n)\n\ntype bodyStreamSizeReader struct {\n\treader io.Reader\n\tonEOF  func(read int64)\n\tread   int64\n\teof    sync.Once\n}\n\nfunc (b *bodyStreamSizeReader) Read(p []byte) (n int, err error) {\n\tn, err = b.reader.Read(p)\n\tif n > 0 {\n\t\tatomic.AddInt64(&b.read, int64(n))\n\t}\n\tif err == io.EOF && b.onEOF != nil {\n\t\tread := atomic.LoadInt64(&b.read)\n\t\tb.eof.Do(func() {\n\t\t\tb.onEOF(read)\n\t\t})\n\t}\n\n\treturn n, err\n}\n\nfunc (b *bodyStreamSizeReader) Close() error {\n\tcloser, ok := b.reader.(io.Closer)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn closer.Close()\n}\n\nfunc detachedMetricContext(ctx context.Context) context.Context {\n\tdetached := context.Background()\n\n\tif spanContext := oteltrace.SpanContextFromContext(ctx); spanContext.IsValid() {\n\t\tdetached = oteltrace.ContextWithSpanContext(detached, spanContext)\n\t}\n\n\tif bg := baggage.FromContext(ctx); bg.Len() > 0 {\n\t\tdetached = baggage.ContextWithBaggage(detached, bg)\n\t}\n\n\treturn detached\n}\n\n// Middleware returns fiber handler which will trace incoming requests.\nfunc Middleware(opts ...Option) fiber.Handler {\n\tcfg := config{\n\t\tclientIP: true,\n\t}\n\tfor _, opt := range opts {\n\t\topt.apply(&cfg)\n\t}\n\n\tif cfg.TracerProvider == nil {\n\t\tcfg.TracerProvider = otel.GetTracerProvider()\n\t}\n\ttracer := cfg.TracerProvider.Tracer(\n\t\tinstrumentationName,\n\t\toteltrace.WithInstrumentationVersion(otelcontrib.Version()),\n\t)\n\n\tvar httpServerDuration metric.Float64Histogram\n\tvar httpServerRequestSize metric.Int64Histogram\n\tvar httpServerResponseSize metric.Int64Histogram\n\tvar httpServerActiveRequests metric.Int64UpDownCounter\n\n\tif !cfg.withoutMetrics {\n\t\tif cfg.MeterProvider == nil {\n\t\t\tcfg.MeterProvider = otel.GetMeterProvider()\n\t\t}\n\t\tmeter := cfg.MeterProvider.Meter(\n\t\t\tinstrumentationName,\n\t\t\tmetric.WithInstrumentationVersion(otelcontrib.Version()),\n\t\t)\n\n\t\tvar err error\n\t\thttpServerDuration, err = meter.Float64Histogram(MetricNameHTTPServerRequestDuration, metric.WithUnit(UnitSeconds), metric.WithDescription(\"Duration of HTTP server requests.\"))\n\t\tif err != nil {\n\t\t\totel.Handle(err)\n\t\t}\n\t\thttpServerRequestSize, err = meter.Int64Histogram(MetricNameHTTPServerRequestBodySize, metric.WithUnit(UnitBytes), metric.WithDescription(\"Size of HTTP server request bodies.\"))\n\t\tif err != nil {\n\t\t\totel.Handle(err)\n\t\t}\n\t\thttpServerResponseSize, err = meter.Int64Histogram(MetricNameHTTPServerResponseBodySize, metric.WithUnit(UnitBytes), metric.WithDescription(\"Size of HTTP server response bodies.\"))\n\t\tif err != nil {\n\t\t\totel.Handle(err)\n\t\t}\n\t\thttpServerActiveRequests, err = meter.Int64UpDownCounter(MetricNameHTTPServerActiveRequests, metric.WithUnit(UnitDimensionless), metric.WithDescription(\"Number of active HTTP server requests.\"))\n\t\tif err != nil {\n\t\t\totel.Handle(err)\n\t\t}\n\t}\n\n\tif cfg.Propagators == nil {\n\t\tcfg.Propagators = otel.GetTextMapPropagator()\n\t}\n\tif cfg.SpanNameFormatter == nil {\n\t\tcfg.SpanNameFormatter = defaultSpanNameFormatter\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\tfiber.StoreInContext(c, tracerKey, tracer)\n\t\tsavedCtx, cancel := context.WithCancel(c.Context())\n\n\t\tstart := time.Now()\n\n\t\trequestMetricsAttrs := httpServerMetricAttributesFromRequest(c, cfg)\n\t\tif !cfg.withoutMetrics {\n\t\t\thttpServerActiveRequests.Add(savedCtx, 1, metric.WithAttributes(requestMetricsAttrs...))\n\t\t}\n\n\t\tresponseMetricAttrs := make([]attribute.KeyValue, len(requestMetricsAttrs))\n\t\tcopy(responseMetricAttrs, requestMetricsAttrs)\n\n\t\trequest := c.Request()\n\t\tisRequestBodyStream := request.IsBodyStream()\n\t\trequestSize := int64(0)\n\t\tvar requestBodyStreamSizeReader *bodyStreamSizeReader\n\t\tif isRequestBodyStream && !cfg.withoutMetrics {\n\t\t\trequestBodyStream := request.BodyStream()\n\t\t\tif requestBodyStream != nil {\n\t\t\t\trequestBodyStreamSizeReader = &bodyStreamSizeReader{reader: requestBodyStream}\n\t\t\t\trequest.SetBodyStream(requestBodyStreamSizeReader, -1)\n\t\t\t}\n\t\t} else {\n\t\t\trequestSize = int64(len(request.Body()))\n\t\t}\n\n\t\treqHeader := make(http.Header)\n\t\tfor header, values := range c.GetReqHeaders() {\n\t\t\tfor _, value := range values {\n\t\t\t\treqHeader.Add(header, value)\n\t\t\t}\n\t\t}\n\n\t\tctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(reqHeader))\n\n\t\topts := []oteltrace.SpanStartOption{\n\t\t\toteltrace.WithAttributes(httpServerTraceAttributesFromRequest(c, cfg)...),\n\t\t\toteltrace.WithSpanKind(oteltrace.SpanKindServer),\n\t\t}\n\n\t\t// temporary set to c.Path() first\n\t\t// update with c.Route().Path after c.Next() is called\n\t\t// to get pathRaw\n\t\tspanName := utils.CopyString(c.Path())\n\t\tctx, span := tracer.Start(ctx, spanName, opts...)\n\t\tdefer span.End()\n\n\t\t// pass the span through userContext\n\t\tc.SetContext(ctx)\n\n\t\t// serve the request to the next middleware\n\t\tif err := c.Next(); err != nil {\n\t\t\tspan.RecordError(err)\n\t\t\t// invokes the registered HTTP error handler\n\t\t\t// to get the correct response status code\n\t\t\t_ = c.App().Config().ErrorHandler(c, err)\n\t\t}\n\n\t\t// extract common attributes from response\n\t\tresponseAttrs := []attribute.KeyValue{\n\t\t\tsemconv.HTTPResponseStatusCode(c.Response().StatusCode()),\n\t\t\tsemconv.HTTPRouteKey.String(c.Route().Path), // no need to copy c.Route().Path: route strings should be immutable across app lifecycle\n\t\t}\n\n\t\tresponse := c.Response()\n\t\tisSSE := c.GetRespHeader(\"Content-Type\") == \"text/event-stream\"\n\t\tresponseSize := int64(0)\n\t\tisResponseBodyStream := response.IsBodyStream()\n\t\tif !isResponseBodyStream && !isSSE {\n\t\t\tresponseSize = int64(len(response.Body()))\n\t\t}\n\n\t\tif isResponseBodyStream && !isSSE && !cfg.withoutMetrics {\n\t\t\tresponseBodyStream := response.BodyStream()\n\t\t\tif responseBodyStream != nil {\n\t\t\t\tresponseMetricAttrsWithResponse := append(responseMetricAttrs, responseAttrs...)\n\t\t\t\tresponseMetricsCtx := detachedMetricContext(savedCtx)\n\t\t\t\tresponseBodyStreamReader := &bodyStreamSizeReader{\n\t\t\t\t\treader: responseBodyStream,\n\t\t\t\t\tonEOF: func(read int64) {\n\t\t\t\t\t\thttpServerResponseSize.Record(responseMetricsCtx, read, metric.WithAttributes(responseMetricAttrsWithResponse...))\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tresponse.SetBodyStream(responseBodyStreamReader, -1)\n\t\t\t} else {\n\t\t\t\tisResponseBodyStream = false\n\t\t\t}\n\t\t}\n\n\t\tdefer func() {\n\t\t\tresponseMetricAttrs = append(responseMetricAttrs, responseAttrs...)\n\t\t\tif requestBodyStreamSizeReader != nil {\n\t\t\t\trequestSize = atomic.LoadInt64(&requestBodyStreamSizeReader.read)\n\t\t\t}\n\n\t\t\tif !cfg.withoutMetrics {\n\t\t\t\thttpServerActiveRequests.Add(savedCtx, -1, metric.WithAttributes(requestMetricsAttrs...))\n\t\t\t\thttpServerDuration.Record(savedCtx, time.Since(start).Seconds(), metric.WithAttributes(responseMetricAttrs...))\n\t\t\t\thttpServerRequestSize.Record(savedCtx, requestSize, metric.WithAttributes(responseMetricAttrs...))\n\t\t\t\tif !isResponseBodyStream {\n\t\t\t\t\thttpServerResponseSize.Record(savedCtx, responseSize, metric.WithAttributes(responseMetricAttrs...))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tc.SetContext(savedCtx)\n\t\t\tcancel()\n\t\t}()\n\n\t\tif !isResponseBodyStream {\n\t\t\tspan.SetAttributes(append(responseAttrs, semconv.HTTPResponseBodySizeKey.Int64(responseSize))...)\n\t\t} else {\n\t\t\tspan.SetAttributes(responseAttrs...)\n\t\t}\n\t\tspan.SetName(cfg.SpanNameFormatter(c))\n\n\t\tspanStatus, spanMessage := internal.SpanStatusFromHTTPStatusCodeAndSpanKind(c.Response().StatusCode(), oteltrace.SpanKindServer)\n\t\tspan.SetStatus(spanStatus, spanMessage)\n\n\t\t//Propagate tracing context as headers in outbound response\n\t\ttracingHeaders := make(propagation.HeaderCarrier)\n\t\tcfg.Propagators.Inject(c.Context(), tracingHeaders)\n\t\tfor _, headerKey := range tracingHeaders.Keys() {\n\t\t\tc.Set(headerKey, tracingHeaders.Get(headerKey))\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// defaultSpanNameFormatter is the default formatter for spans created with the fiber\n// integration. Returns the route pathRaw\nfunc defaultSpanNameFormatter(ctx fiber.Ctx) string {\n\treturn ctx.Route().Path\n}\n"
  },
  {
    "path": "v3/otel/fiber_context_test.go",
    "content": "package otel\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\toteltrace \"go.opentelemetry.io/otel/trace\"\n)\n\nfunc TestMiddleware_StoreTracerInContextWithPassLocalsToContext(t *testing.T) {\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(Middleware())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttracerFromContext, ok := fiber.ValueFromContext[oteltrace.Tracer](c.Context(), tracerKey)\n\t\trequire.True(t, ok)\n\t\trequire.NotNil(t, tracerFromContext)\n\t\treturn c.SendStatus(http.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/\", nil))\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n}\n"
  },
  {
    "path": "v3/otel/go.mod",
    "content": "module github.com/gofiber/contrib/v3/otel\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/stretchr/testify v1.11.1\n\tgo.opentelemetry.io/contrib v1.43.0\n\tgo.opentelemetry.io/contrib/propagators/b3 v1.43.0\n\tgo.opentelemetry.io/otel v1.43.0\n\tgo.opentelemetry.io/otel/metric v1.43.0\n\tgo.opentelemetry.io/otel/sdk v1.43.0\n\tgo.opentelemetry.io/otel/sdk/metric v1.43.0\n\tgo.opentelemetry.io/otel/trace v1.43.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/otel/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib v1.43.0 h1:rv+pngknCr4qpZDxSpEvEoRioutgfbkk82x6MChJQ3U=\ngo.opentelemetry.io/contrib v1.43.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk=\ngo.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=\ngo.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=\ngo.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=\ngo.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=\ngo.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=\ngo.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=\ngo.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=\ngo.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=\ngo.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=\ngo.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/otel/internal/http.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\n// SpanStatusFromHTTPStatusCodeAndSpanKind generates a status code and a message\n// as specified by the OpenTelemetry specification for a span.\n// Exclude 4xx for SERVER to set the appropriate status.\nfunc SpanStatusFromHTTPStatusCodeAndSpanKind(code int, spanKind trace.SpanKind) (codes.Code, string) {\n\t// This code block ignores the HTTP 306 status code. The 306 status code is no longer in use.\n\tif http.StatusText(code) == \"\" {\n\t\treturn codes.Error, fmt.Sprintf(\"Invalid HTTP status code %d\", code)\n\t}\n\n\tif (code >= http.StatusContinue && code < http.StatusBadRequest) ||\n\t\t(spanKind == trace.SpanKindServer && isCode4xx(code)) {\n\t\treturn codes.Unset, \"\"\n\t}\n\treturn codes.Error, \"\"\n}\n\nfunc isCode4xx(code int) bool {\n\treturn code >= http.StatusBadRequest && code <= http.StatusUnavailableForLegalReasons\n}\n"
  },
  {
    "path": "v3/otel/internal/http_test.go",
    "content": "package internal\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.opentelemetry.io/otel/codes\"\n\toteltrace \"go.opentelemetry.io/otel/trace\"\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestIsCode4xxIsNotValid(t *testing.T) {\n\tresponse := isCode4xx(http.StatusOK)\n\n\tassert.False(t, response)\n}\n\nfunc TestIsCode4xxIsValid(t *testing.T) {\n\tresponse := isCode4xx(http.StatusNotFound)\n\n\tassert.True(t, response)\n}\n\nfunc TestStatusErrorWithMessage(t *testing.T) {\n\tspanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(600, oteltrace.SpanKindClient)\n\n\tassert.Equal(t, codes.Error, spanStatus)\n\tassert.Equal(t, \"Invalid HTTP status code 600\", spanMessage)\n}\n\nfunc TestStatusErrorWithMessageForIgnoredHTTPCode(t *testing.T) {\n\tspanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(306, oteltrace.SpanKindClient)\n\n\tassert.Equal(t, codes.Error, spanStatus)\n\tassert.Equal(t, \"Invalid HTTP status code 306\", spanMessage)\n}\n\nfunc TestStatusErrorWhenHTTPCode5xx(t *testing.T) {\n\tspanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(http.StatusInternalServerError, oteltrace.SpanKindServer)\n\n\tassert.Equal(t, codes.Error, spanStatus)\n\tassert.Equal(t, \"\", spanMessage)\n}\n\nfunc TestStatusUnsetWhenServerSpanAndBadRequest(t *testing.T) {\n\tspanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(http.StatusBadRequest, oteltrace.SpanKindServer)\n\n\tassert.Equal(t, codes.Unset, spanStatus)\n\tassert.Equal(t, \"\", spanMessage)\n}\n\nfunc TestStatusUnset(t *testing.T) {\n\tspanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(http.StatusOK, oteltrace.SpanKindClient)\n\n\tassert.Equal(t, codes.Unset, spanStatus)\n\tassert.Equal(t, \"\", spanMessage)\n}\n"
  },
  {
    "path": "v3/otel/otel_test/fiber_test.go",
    "content": "package otel_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\tfiberotel \"github.com/gofiber/contrib/v3/otel\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\totelcontrib \"go.opentelemetry.io/contrib\"\n\tb3prop \"go.opentelemetry.io/contrib/propagators/b3\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/codes\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/sdk/instrumentation\"\n\t\"go.opentelemetry.io/otel/sdk/metric\"\n\t\"go.opentelemetry.io/otel/sdk/metric/metricdata\"\n\t\"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\t\"go.opentelemetry.io/otel/sdk/trace/tracetest\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n\toteltrace \"go.opentelemetry.io/otel/trace\"\n)\n\nconst instrumentationName = \"github.com/gofiber/contrib/v3/otel\"\n\nfunc TestChildSpanFromGlobalTracer(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware())\n\tapp.Get(\"/user/:id\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/user/123\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n}\n\nfunc TestChildSpanFromCustomTracer(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider)))\n\tapp.Get(\"/user/:id\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/user/123\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n}\n\nfunc TestSkipWithNext(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(fiberotel.WithNext(func(c fiber.Ctx) bool {\n\t\treturn c.Path() == \"/health\"\n\t})))\n\n\tapp.Get(\"/health\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/health\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 0)\n}\n\nfunc TestTrace200(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(fiberotel.WithTracerProvider(provider)),\n\t)\n\tapp.Get(\"/user/:id\", func(ctx fiber.Ctx) error {\n\t\tid := ctx.Params(\"id\")\n\t\treturn ctx.SendString(id)\n\t})\n\n\tr := httptest.NewRequest(\"GET\", \"/user/123\", nil)\n\tresp, err := app.Test(r, fiber.TestConfig{Timeout: 3 * time.Second})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\t// do and verify the request\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n\n\t// verify traces look good\n\tspan := spans[0]\n\tattr := span.Attributes()\n\n\tassert.Equal(t, \"/user/:id\", span.Name())\n\tassert.Equal(t, oteltrace.SpanKindServer, span.SpanKind())\n\tassert.Contains(t, attr, attribute.String(\"server.address\", r.Host))\n\tassert.Contains(t, attr, attribute.Int(\"http.response.status_code\", http.StatusOK))\n\tassert.Contains(t, attr, attribute.String(\"http.request.method\", \"GET\"))\n\tassert.Contains(t, attr, attribute.String(\"url.path\", \"/user/123\"))\n\tassert.Contains(t, attr, attribute.String(\"http.route\", \"/user/:id\"))\n}\n\nfunc TestError(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\n\t// setup\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider)))\n\t// configure a handler that returns an error and 5xx status code\n\tapp.Get(\"/server_err\", func(ctx fiber.Ctx) error {\n\t\treturn errors.New(\"oh no\")\n\t})\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/server_err\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\tassert.Equal(t, http.StatusInternalServerError, resp.StatusCode)\n\n\t// verify the errors and status are correct\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n\tspan := spans[0]\n\tattr := span.Attributes()\n\n\tassert.Equal(t, \"/server_err\", span.Name())\n\tassert.Contains(t, attr, attribute.Int(\"http.response.status_code\", http.StatusInternalServerError))\n\tassert.Equal(t, attribute.StringValue(\"oh no\"), span.Events()[0].Attributes[1].Value)\n\t// server errors set the status\n\tassert.Equal(t, codes.Error, span.Status().Code)\n}\n\nfunc TestErrorOnlyHandledOnce(t *testing.T) {\n\ttimesHandlingError := 0\n\tapp := fiber.New(fiber.Config{\n\t\tErrorHandler: func(ctx fiber.Ctx, err error) error {\n\t\t\ttimesHandlingError++\n\t\t\treturn fiber.NewError(http.StatusInternalServerError, err.Error())\n\t\t},\n\t})\n\tapp.Use(fiberotel.Middleware())\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\treturn errors.New(\"mock error\")\n\t})\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tassert.Equal(t, 1, timesHandlingError)\n}\n\nfunc TestGetSpanNotInstrumented(t *testing.T) {\n\tvar gotSpan oteltrace.Span\n\n\tapp := fiber.New()\n\tapp.Get(\"/ping\", func(ctx fiber.Ctx) error {\n\t\t// Assert we don't have a span on the context.\n\t\tgotSpan = oteltrace.SpanFromContext(ctx)\n\t\treturn ctx.SendString(\"ok\")\n\t})\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/ping\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tok := !gotSpan.SpanContext().IsValid()\n\tassert.True(t, ok)\n}\n\nfunc TestPropagationWithGlobalPropagators(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\totel.SetTextMapPropagator(propagation.TraceContext{})\n\tdefer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator())\n\n\tr := httptest.NewRequest(\"GET\", \"/user/123\", nil)\n\n\tctx, pspan := provider.Tracer(instrumentationName).Start(context.Background(), \"test\")\n\totel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(r.Header))\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider)))\n\tapp.Get(\"/user/:id\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\tresp, err := app.Test(r)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n\n\t// verify traces look good\n\tspan := spans[0]\n\tassert.Equal(t, pspan.SpanContext().TraceID(), span.SpanContext().TraceID())\n\tassert.Equal(t, pspan.SpanContext().SpanID(), span.Parent().SpanID())\n}\n\nfunc TestPropagationWithCustomPropagators(t *testing.T) {\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\totel.SetTracerProvider(provider)\n\n\tb3 := b3prop.New()\n\n\tr := httptest.NewRequest(\"GET\", \"/user/123\", nil)\n\n\tctx, pspan := provider.Tracer(instrumentationName).Start(context.Background(), \"test\")\n\tb3.Inject(ctx, propagation.HeaderCarrier(r.Header))\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider), fiberotel.WithPropagators(b3)))\n\tapp.Get(\"/user/:id\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\tresp, err := app.Test(r)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n\tmspan := spans[0]\n\tassert.Equal(t, pspan.SpanContext().TraceID(), mspan.SpanContext().TraceID())\n\tassert.Equal(t, pspan.SpanContext().SpanID(), mspan.Parent().SpanID())\n}\n\nfunc TestHasBasicAuth(t *testing.T) {\n\ttestCases := []struct {\n\t\tdesc  string\n\t\tauth  string\n\t\tuser  string\n\t\tvalid bool\n\t}{\n\t\t{\n\t\t\tdesc:  \"valid header\",\n\t\t\tauth:  \"Basic dXNlcjpwYXNzd29yZA==\",\n\t\t\tuser:  \"user\",\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid header\",\n\t\t\tauth: \"Bas\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"invalid basic header\",\n\t\t\tauth: \"Basic 12345\",\n\t\t},\n\t\t{\n\t\t\tdesc: \"no header\",\n\t\t},\n\t}\n\n\tfor _, tC := range testCases {\n\t\tt.Run(tC.desc, func(t *testing.T) {\n\t\t\tval, valid := fiberotel.HasBasicAuth(tC.auth)\n\n\t\t\tassert.Equal(t, tC.user, val)\n\t\t\tassert.Equal(t, tC.valid, valid)\n\t\t})\n\t}\n}\n\nfunc TestMetric(t *testing.T) {\n\treader := metric.NewManualReader()\n\tprovider := metric.NewMeterProvider(metric.WithReader(reader))\n\n\tport := 8080\n\troute := \"/foo\"\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(\n\t\t\tfiberotel.WithMeterProvider(provider),\n\t\t\tfiberotel.WithPort(port),\n\t\t),\n\t)\n\tapp.Get(route, func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusOK)\n\t})\n\n\tr := httptest.NewRequest(http.MethodGet, route, nil)\n\tresp, err := app.Test(r)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tmetrics := metricdata.ResourceMetrics{}\n\terr = reader.Collect(context.Background(), &metrics)\n\tassert.NoError(t, err)\n\tassert.Len(t, metrics.ScopeMetrics, 1)\n\n\trequestAttrs := []attribute.KeyValue{\n\t\tsemconv.NetworkProtocolName(\"http\"),\n\t\tsemconv.NetworkProtocolVersion(fmt.Sprintf(\"1.%d\", r.ProtoMinor)),\n\t\tsemconv.URLScheme(\"http\"),\n\t\tsemconv.HTTPRequestMethodKey.String(http.MethodGet),\n\t\tsemconv.ServerAddress(r.Host),\n\t\tsemconv.ServerPort(port),\n\t}\n\tresponseAttrs := []attribute.KeyValue{\n\t\tsemconv.HTTPResponseStatusCode(200),\n\t\tsemconv.HTTPRouteKey.String(route),\n\t}\n\n\tassertScopeMetrics(t, metrics.ScopeMetrics[0], route, requestAttrs, append(requestAttrs, responseAttrs...))\n}\n\nfunc assertScopeMetrics(t *testing.T, sm metricdata.ScopeMetrics, route string, requestAttrs []attribute.KeyValue, responseAttrs []attribute.KeyValue) {\n\tassert.Equal(t, instrumentation.Scope{\n\t\tName:    instrumentationName,\n\t\tVersion: otelcontrib.Version(),\n\t}, sm.Scope)\n\n\t// Duration value is not predictable.\n\tm := sm.Metrics[0]\n\tassert.Equal(t, fiberotel.MetricNameHTTPServerRequestDuration, m.Name)\n\tassert.Equal(t, fiberotel.UnitSeconds, m.Unit)\n\trequire.IsType(t, m.Data, metricdata.Histogram[float64]{})\n\thist := m.Data.(metricdata.Histogram[float64])\n\tassert.Equal(t, metricdata.CumulativeTemporality, hist.Temporality)\n\trequire.Len(t, hist.DataPoints, 1)\n\tdp := hist.DataPoints[0]\n\tassert.Equal(t, attribute.NewSet(responseAttrs...), dp.Attributes, \"attributes\")\n\tassert.Equal(t, uint64(1), dp.Count, \"count\")\n\tassert.Less(t, dp.Sum, 0.01) // test shouldn't take longer than 10 milliseconds (0.01 seconds)\n\n\t// Request size\n\twant := metricdata.Metrics{\n\t\tName:        fiberotel.MetricNameHTTPServerRequestBodySize,\n\t\tDescription: \"Size of HTTP server request bodies.\",\n\t\tUnit:        fiberotel.UnitBytes,\n\t\tData:        getHistogram(0, responseAttrs),\n\t}\n\tmetricdatatest.AssertEqual(t, want, sm.Metrics[1], metricdatatest.IgnoreTimestamp())\n\n\t// Response size\n\twant = metricdata.Metrics{\n\t\tName:        fiberotel.MetricNameHTTPServerResponseBodySize,\n\t\tDescription: \"Size of HTTP server response bodies.\",\n\t\tUnit:        fiberotel.UnitBytes,\n\t\tData:        getHistogram(2, responseAttrs),\n\t}\n\tmetricdatatest.AssertEqual(t, want, sm.Metrics[2], metricdatatest.IgnoreTimestamp())\n\n\t// Active requests\n\twant = metricdata.Metrics{\n\t\tName:        fiberotel.MetricNameHTTPServerActiveRequests,\n\t\tDescription: \"Number of active HTTP server requests.\",\n\t\tUnit:        fiberotel.UnitDimensionless,\n\t\tData: metricdata.Sum[int64]{\n\t\t\tDataPoints: []metricdata.DataPoint[int64]{\n\t\t\t\t{Attributes: attribute.NewSet(requestAttrs...), Value: 0},\n\t\t\t},\n\t\t\tTemporality: metricdata.CumulativeTemporality,\n\t\t},\n\t}\n\tmetricdatatest.AssertEqual(t, want, sm.Metrics[3], metricdatatest.IgnoreTimestamp())\n}\n\nfunc getHistogram(value float64, attrs []attribute.KeyValue) metricdata.Histogram[int64] {\n\tbounds := []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000}\n\tbucketCounts := make([]uint64, len(bounds)+1)\n\n\tfor i, v := range bounds {\n\t\tif value <= v {\n\t\t\tbucketCounts[i]++\n\t\t\tbreak\n\t\t}\n\n\t\tif i == len(bounds)-1 {\n\t\t\tbounds[i+1]++\n\t\t\tbreak\n\t\t}\n\t}\n\n\textremaValue := metricdata.NewExtrema[int64](int64(value))\n\n\treturn metricdata.Histogram[int64]{\n\t\tDataPoints: []metricdata.HistogramDataPoint[int64]{\n\t\t\t{\n\t\t\t\tAttributes:   attribute.NewSet(attrs...),\n\t\t\t\tBounds:       bounds,\n\t\t\t\tBucketCounts: bucketCounts,\n\t\t\t\tCount:        1,\n\t\t\t\tMin:          extremaValue,\n\t\t\t\tMax:          extremaValue,\n\t\t\t\tSum:          int64(value),\n\t\t\t},\n\t\t},\n\t\tTemporality: metricdata.CumulativeTemporality,\n\t}\n}\n\nfunc TestCustomAttributes(t *testing.T) {\n\tsr := new(tracetest.SpanRecorder)\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(\n\t\t\tfiberotel.WithTracerProvider(provider),\n\t\t\tfiberotel.WithCustomAttributes(func(ctx fiber.Ctx) []attribute.KeyValue {\n\t\t\t\treturn []attribute.KeyValue{\n\t\t\t\t\tattribute.Key(\"http.query_params\").String(ctx.Request().URI().QueryArgs().String()),\n\t\t\t\t}\n\t\t\t}),\n\t\t),\n\t)\n\n\tapp.Get(\"/user/:id\", func(ctx fiber.Ctx) error {\n\t\tid := ctx.Params(\"id\")\n\t\treturn ctx.SendString(id)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/user/123?foo=bar\", nil), fiber.TestConfig{Timeout: 3 * time.Second})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\t// do and verify the request\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tspans := sr.Ended()\n\trequire.Len(t, spans, 1)\n\n\t// verify traces look good\n\tspan := spans[0]\n\tattr := span.Attributes()\n\n\tassert.Equal(t, \"/user/:id\", span.Name())\n\tassert.Equal(t, oteltrace.SpanKindServer, span.SpanKind())\n\tassert.Contains(t, attr, attribute.Int(\"http.response.status_code\", http.StatusOK))\n\tassert.Contains(t, attr, attribute.String(\"http.request.method\", \"GET\"))\n\tassert.Contains(t, attr, attribute.String(\"url.path\", \"/user/123\"))\n\tassert.Contains(t, attr, attribute.String(\"http.route\", \"/user/:id\"))\n\tassert.Contains(t, attr, semconv.URLQuery(\"foo=bar\"))\n}\n\nfunc TestCustomMetricAttributes(t *testing.T) {\n\treader := metric.NewManualReader()\n\tprovider := metric.NewMeterProvider(metric.WithReader(reader))\n\n\tport := 8080\n\troute := \"/foo\"\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(\n\t\t\tfiberotel.WithMeterProvider(provider),\n\t\t\tfiberotel.WithPort(port),\n\t\t\tfiberotel.WithCustomMetricAttributes(func(ctx fiber.Ctx) []attribute.KeyValue {\n\t\t\t\treturn []attribute.KeyValue{semconv.URLQuery(ctx.Request().URI().QueryArgs().String())}\n\t\t\t}),\n\t\t),\n\t)\n\n\tapp.Get(route, func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusOK)\n\t})\n\n\tr := httptest.NewRequest(http.MethodGet, \"/foo?foo=bar\", nil)\n\tresp, err := app.Test(r)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\t// do and verify the request\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tmetrics := metricdata.ResourceMetrics{}\n\terr = reader.Collect(context.Background(), &metrics)\n\tassert.NoError(t, err)\n\tassert.Len(t, metrics.ScopeMetrics, 1)\n\n\trequestAttrs := []attribute.KeyValue{\n\t\tsemconv.NetworkProtocolName(\"http\"),\n\t\tsemconv.NetworkProtocolVersion(fmt.Sprintf(\"1.%d\", r.ProtoMinor)),\n\t\tsemconv.HTTPRequestMethodKey.String(http.MethodGet),\n\t\tsemconv.URLSchemeKey.String(\"http\"),\n\t\tsemconv.ServerAddress(r.Host),\n\t\tsemconv.ServerPort(port),\n\t\tsemconv.URLQuery(\"foo=bar\"),\n\t}\n\tresponseAttrs := []attribute.KeyValue{\n\t\tsemconv.HTTPResponseStatusCode(200),\n\t\tsemconv.HTTPRouteKey.String(route),\n\t}\n\n\tassertScopeMetrics(t, metrics.ScopeMetrics[0], route, requestAttrs, append(requestAttrs, responseAttrs...))\n}\n\nfunc TestOutboundTracingPropagation(t *testing.T) {\n\tsr := new(tracetest.SpanRecorder)\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(\n\t\tfiberotel.WithTracerProvider(provider),\n\t\tfiberotel.WithPropagators(b3prop.New(b3prop.WithInjectEncoding(b3prop.B3MultipleHeader))),\n\t))\n\tapp.Get(\"/foo\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/foo\", nil), fiber.TestConfig{Timeout: 3 * time.Second})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tassert.Equal(t, \"1\", resp.Header.Get(\"X-B3-Sampled\"))\n\tassert.NotEmpty(t, resp.Header.Get(\"X-B3-SpanId\"))\n\tassert.NotEmpty(t, resp.Header.Get(\"X-B3-TraceId\"))\n\n}\n\nfunc TestOutboundTracingPropagationWithInboundContext(t *testing.T) {\n\tconst spanId = \"619907d88b766fb8\"\n\tconst traceId = \"813dd2766ff711bf02b60e9883014964\"\n\n\tsr := new(tracetest.SpanRecorder)\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\n\tapp := fiber.New()\n\tapp.Use(fiberotel.Middleware(\n\t\tfiberotel.WithTracerProvider(provider),\n\t\tfiberotel.WithPropagators(b3prop.New(b3prop.WithInjectEncoding(b3prop.B3MultipleHeader))),\n\t))\n\tapp.Get(\"/foo\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/foo\", nil)\n\n\treq.Header.Set(\"X-B3-SpanId\", spanId)\n\treq.Header.Set(\"X-B3-TraceId\", traceId)\n\treq.Header.Set(\"X-B3-Sampled\", \"1\")\n\n\tresp, err := app.Test(req, fiber.TestConfig{Timeout: 3 * time.Second})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tassert.NotEmpty(t, resp.Header.Get(\"X-B3-SpanId\"))\n\tassert.Equal(t, traceId, resp.Header.Get(\"X-B3-TraceId\"))\n\tassert.Equal(t, \"1\", resp.Header.Get(\"X-B3-Sampled\"))\n}\n\nfunc TestCollectClientIP(t *testing.T) {\n\tt.Parallel()\n\n\toptFactories := []struct {\n\t\tname string\n\t\topt  func(bool) fiberotel.Option\n\t}{\n\t\t{name: \"WithClientIP\", opt: fiberotel.WithClientIP},\n\t\t{name: \"WithCollectClientIP\", opt: fiberotel.WithCollectClientIP},\n\t}\n\n\tfor _, factory := range optFactories {\n\t\tfactory := factory\n\t\tt.Run(factory.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfor _, enabled := range []bool{true, false} {\n\t\t\t\tenabled := enabled\n\t\t\t\tt.Run(fmt.Sprintf(\"enabled=%t\", enabled), func(t *testing.T) {\n\t\t\t\t\tt.Parallel()\n\n\t\t\t\t\tsr := tracetest.NewSpanRecorder()\n\t\t\t\t\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\n\t\t\t\t\tapp := fiber.New()\n\t\t\t\t\tapp.Use(fiberotel.Middleware(\n\t\t\t\t\t\tfiberotel.WithTracerProvider(provider),\n\t\t\t\t\t\tfactory.opt(enabled),\n\t\t\t\t\t))\n\t\t\t\t\tapp.Get(\"/foo\", func(ctx fiber.Ctx) error {\n\t\t\t\t\t\treturn ctx.SendStatus(http.StatusNoContent)\n\t\t\t\t\t})\n\n\t\t\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", nil)\n\t\t\t\t\tresp, err := app.Test(req)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.NotNil(t, resp)\n\n\t\t\t\t\tspans := sr.Ended()\n\t\t\t\t\trequire.Len(t, spans, 1)\n\n\t\t\t\t\tspan := spans[0]\n\t\t\t\t\tattrs := span.Attributes()\n\t\t\t\t\tif enabled {\n\t\t\t\t\t\tassert.Contains(t, attrs, attribute.String(\"client.address\", \"0.0.0.0\"))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NotContains(t, attrs, attribute.String(\"client.address\", \"0.0.0.0\"))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMiddlewarePreservesUserContext(t *testing.T) {\n\ttype ctxKey string\n\tconst requestIDKey ctxKey = \"request_id\"\n\tconst expectedID = 1234\n\n\tsr := tracetest.NewSpanRecorder()\n\tprovider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))\n\n\tapp := fiber.New()\n\t// Middleware that injects a value into the context before otel\n\tapp.Use(func(c fiber.Ctx) error {\n\t\tctx := context.WithValue(c.Context(), requestIDKey, expectedID)\n\t\tc.SetContext(ctx)\n\t\treturn c.Next()\n\t})\n\tapp.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider)))\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tval := c.Context().Value(requestIDKey)\n\t\tif val == nil {\n\t\t\treturn c.SendString(\"request_id NOT found in context\")\n\t\t}\n\t\treturn c.SendString(fmt.Sprintf(\"request_id from context: %d\", val.(int)))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/\", nil))\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, fmt.Sprintf(\"request_id from context: %d\", expectedID), string(body))\n}\n\nfunc TestWithoutMetrics(t *testing.T) {\n\treader := metric.NewManualReader()\n\tprovider := metric.NewMeterProvider(metric.WithReader(reader))\n\n\tport := 8080\n\troute := \"/foo\"\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(\n\t\t\tfiberotel.WithMeterProvider(provider),\n\t\t\tfiberotel.WithPort(port),\n\t\t\tfiberotel.WithoutMetrics(true),\n\t\t),\n\t)\n\tapp.Get(route, func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStatus(http.StatusOK)\n\t})\n\n\tr := httptest.NewRequest(http.MethodGet, route, nil)\n\tresp, err := app.Test(r)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tmetrics := metricdata.ResourceMetrics{}\n\terr = reader.Collect(context.Background(), &metrics)\n\tassert.NoError(t, err)\n\tassert.Len(t, metrics.ScopeMetrics, 0, \"No metrics should be collected when metrics are disabled\")\n}\n\nfunc TestWithoutMetricsWithStreamResponse(t *testing.T) {\n\treader := metric.NewManualReader()\n\tprovider := metric.NewMeterProvider(metric.WithReader(reader))\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(\n\t\t\tfiberotel.WithMeterProvider(provider),\n\t\t\tfiberotel.WithoutMetrics(true),\n\t\t),\n\t)\n\n\tapp.Get(\"/stream\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendStream(bytes.NewReader(make([]byte, 2048)))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/stream\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tassert.Len(t, body, 2048)\n\n\tmetrics := metricdata.ResourceMetrics{}\n\terr = reader.Collect(context.Background(), &metrics)\n\tassert.NoError(t, err)\n\tassert.Len(t, metrics.ScopeMetrics, 0, \"No metrics should be collected when metrics are disabled\")\n}\n\nfunc TestResponseBodySizeWithStream(t *testing.T) {\n\tconst responseBodySize = 8192\n\n\treader := metric.NewManualReader()\n\tprovider := metric.NewMeterProvider(metric.WithReader(reader))\n\n\tapp := fiber.New()\n\tapp.Use(\n\t\tfiberotel.Middleware(\n\t\t\tfiberotel.WithMeterProvider(provider),\n\t\t),\n\t)\n\n\tapp.Get(\"/stream\", func(ctx fiber.Ctx) error {\n\t\tpayload := make([]byte, responseBodySize)\n\t\treturn ctx.SendStream(bytes.NewReader(payload))\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(http.MethodGet, \"/stream\", nil))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, resp)\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\tassert.Len(t, body, responseBodySize)\n\n\tmetrics := metricdata.ResourceMetrics{}\n\terr = reader.Collect(context.Background(), &metrics)\n\trequire.NoError(t, err)\n\trequire.Len(t, metrics.ScopeMetrics, 1)\n\n\tvar got metricdata.Histogram[int64]\n\tfor _, m := range metrics.ScopeMetrics[0].Metrics {\n\t\tif m.Name == fiberotel.MetricNameHttpServerResponseSize {\n\t\t\tvar ok bool\n\t\t\tgot, ok = m.Data.(metricdata.Histogram[int64])\n\t\t\trequire.True(t, ok)\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.Len(t, got.DataPoints, 1)\n\tassert.Equal(t, int64(responseBodySize), got.DataPoints[0].Sum)\n}\n"
  },
  {
    "path": "v3/otel/semconv.go",
    "content": "package otel\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.39.0\"\n)\n\nvar (\n\thttpProtocolNameAttr = semconv.NetworkProtocolName(\"http\")\n\thttp11VersionAttr    = semconv.NetworkProtocolVersion(\"1.1\")\n\thttp10VersionAttr    = semconv.NetworkProtocolVersion(\"1.0\")\n\tenduserIDKey         = attribute.Key(\"enduser.id\")\n)\n\nfunc httpServerMetricAttributesFromRequest(c fiber.Ctx, cfg config) []attribute.KeyValue {\n\tprotocolAttributes := httpNetworkProtocolAttributes(c)\n\tattrs := []attribute.KeyValue{\n\t\tsemconv.URLScheme(requestScheme(c)),\n\t\tsemconv.ServerAddress(utils.CopyString(c.Hostname())),\n\t\tsemconv.HTTPRequestMethodKey.String(utils.CopyString(c.Method())),\n\t}\n\tattrs = append(attrs, protocolAttributes...)\n\n\tif cfg.Port != nil {\n\t\tattrs = append(attrs, semconv.ServerPort(*cfg.Port))\n\t}\n\n\tif cfg.CustomMetricAttributes != nil {\n\t\tattrs = append(attrs, cfg.CustomMetricAttributes(c)...)\n\t}\n\n\treturn attrs\n}\n\nfunc httpServerTraceAttributesFromRequest(c fiber.Ctx, cfg config) []attribute.KeyValue {\n\tprotocolAttributes := httpNetworkProtocolAttributes(c)\n\tattrs := []attribute.KeyValue{\n\t\t// utils.CopyString: we need to copy the string as fasthttp strings are by default\n\t\t// mutable so it will be unsafe to use in this middleware as it might be used after\n\t\t// the handler returns.\n\t\tsemconv.HTTPRequestMethodKey.String(utils.CopyString(c.Method())),\n\t\tsemconv.URLScheme(requestScheme(c)),\n\t\tsemconv.HTTPRequestBodySize(c.Request().Header.ContentLength()),\n\t\tsemconv.URLPath(string(utils.CopyBytes(c.Request().URI().Path()))),\n\t\tsemconv.URLQuery(c.Request().URI().QueryArgs().String()),\n\t\tsemconv.URLFull(utils.CopyString(c.OriginalURL())),\n\t\tsemconv.UserAgentOriginal(string(utils.CopyBytes(c.Request().Header.UserAgent()))),\n\t\tsemconv.ServerAddress(utils.CopyString(c.Hostname())),\n\t\tsemconv.NetworkTransportTCP,\n\t}\n\tattrs = append(attrs, protocolAttributes...)\n\n\tif cfg.Port != nil {\n\t\tattrs = append(attrs, semconv.ServerPort(*cfg.Port))\n\t}\n\n\tif username, ok := HasBasicAuth(c.Get(fiber.HeaderAuthorization)); ok {\n\t\tattrs = append(attrs, enduserIDKey.String(username))\n\t}\n\n\tif cfg.clientIP {\n\t\tclientIP := c.IP()\n\t\tif len(clientIP) > 0 {\n\t\t\tattrs = append(attrs, semconv.ClientAddress(utils.CopyString(clientIP)))\n\t\t}\n\t}\n\n\tif cfg.CustomAttributes != nil {\n\t\tattrs = append(attrs, cfg.CustomAttributes(c)...)\n\t}\n\n\treturn attrs\n}\n\nfunc httpNetworkProtocolAttributes(c fiber.Ctx) []attribute.KeyValue {\n\thttpProtocolAttributes := []attribute.KeyValue{httpProtocolNameAttr}\n\tif c.Request().Header.IsHTTP11() {\n\t\treturn append(httpProtocolAttributes, http11VersionAttr)\n\t}\n\treturn append(httpProtocolAttributes, http10VersionAttr)\n}\n\nfunc requestScheme(c fiber.Ctx) string {\n\tscheme := c.Request().URI().Scheme()\n\tif len(scheme) == 0 {\n\t\treturn \"http\"\n\t}\n\n\treturn utils.CopyString(string(scheme))\n}\n\nfunc HasBasicAuth(auth string) (string, bool) {\n\tif auth == \"\" {\n\t\treturn \"\", false\n\t}\n\n\t// Check if the Authorization header is Basic.\n\t// Auth schemes are case-insensitive.\n\tif len(auth) < 6 || !utils.EqualFold(auth[:6], \"Basic \") {\n\t\treturn \"\", false\n\t}\n\n\t// Decode the header contents\n\traw, err := base64.StdEncoding.DecodeString(auth[6:])\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\n\t// Check if the decoded credentials are in the correct form\n\t// which is \"username:password\".\n\tindex := bytes.IndexByte(raw, ':')\n\tif index == -1 {\n\t\treturn \"\", false\n\t}\n\n\t// Get the username\n\treturn string(raw[:index]), true\n}\n"
  },
  {
    "path": "v3/paseto/README.md",
    "content": "---\nid: paseto\n---\n\n# Paseto\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*paseto*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20paseto/badge.svg)\n\nPASETO returns a Web Token (PASETO) auth middleware.\n\n- For valid token, it sets the payload data in Ctx.Locals (and in the underlying `context.Context` when `PassLocalsToContext` is enabled) and calls next handler.\n- For invalid token, it returns \"401 - Unauthorized\" error.\n- For missing token, it returns \"400 - BadRequest\" error.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/paseto\ngo get -u github.com/o1egl/paseto\n```\n\n## Signature\n\n```go\npasetoware.New(config ...pasetoware.Config) func(fiber.Ctx) error\npasetoware.FromContext(ctx any) interface{}\n```\n\n`FromContext` accepts a `fiber.Ctx`, `fiber.CustomCtx`, `*fasthttp.RequestCtx`, or a standard `context.Context` (e.g. the value returned by `c.Context()` when `PassLocalsToContext` is enabled).\n\n## Config\n\n| Property       | Type                            | Description                                                                                                                                                                                             | Default                         |\n|:---------------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------|\n| Next           | `func(fiber.Ctx) bool`          | Defines a function to skip this middleware when it returns true.                                                                                                                                        | `nil`                           |\n| SuccessHandler | `func(fiber.Ctx) error`         | SuccessHandler defines a function which is executed for a valid token.                                                                                                                                  | `c.Next()`                      |\n| ErrorHandler   | `func(fiber.Ctx, error) error`  | ErrorHandler defines a function which is executed for an invalid token.                                                                                                                                 | `401 Invalid or expired PASETO` |\n| Validate       | `PayloadValidator`              | Defines a function to validate if payload is valid. Optional. In case payload used is created using `CreateToken` function. If token is created using another function, this function must be provided. | `nil`                           |\n| SymmetricKey   | `[]byte`                        | Secret key to encrypt token. If present the middleware will generate local tokens.                                                                                                                      | `nil`                           |\n| PrivateKey     | `ed25519.PrivateKey`            | Secret key to sign the tokens. If present (along with its `PublicKey`) the middleware will generate public tokens.                                                                                      | `nil`                           |  \n| PublicKey      | `crypto.PublicKey`              | Public key to verify the tokens. If present (along with `PrivateKey`) the middleware will generate public tokens.                                                                                       | `nil`                           |  \n| Extractor      | `Extractor`                     | Extractor defines a function to extract the token from the request.                                                                                                                                     | `FromAuthHeader(\"Bearer\")`      |\n\n## Available Extractors\n\nPASETO middleware uses the shared Fiber extractors (github.com/gofiber/fiber/v3/extractors) and provides several helpers for different token sources:\n\nImport them like this:\n\n```go\nimport \"github.com/gofiber/fiber/v3/extractors\"\n```\n\nFor an overview and additional examples, see the Fiber Extractors guide:\n\n- https://docs.gofiber.io/guide/extractors\n\n- `extractors.FromAuthHeader(prefix string)` - Extracts token from the Authorization header using the given scheme prefix (e.g., \"Bearer\"). **This is the recommended and most secure method.**\n- `extractors.FromHeader(header string)` - Extracts token from the specified HTTP header\n- `extractors.FromQuery(param string)` - Extracts token from URL query parameters\n- `extractors.FromParam(param string)` - Extracts token from URL path parameters\n- `extractors.FromCookie(key string)` - Extracts token from cookies\n- `extractors.FromForm(param string)` - Extracts token from form data\n- `extractors.Chain(extrs ...extractors.Extractor)` - Tries multiple extractors in order until one succeeds\n\n### Security Considerations\n\n⚠️ **Security Warning**: When choosing an extractor, consider the security implications:\n\n- **URL-based extractors** (`FromQuery`, `FromParam`): Tokens can leak through server logs, browser referrer headers, proxy logs, and browser history. Use only for development or when security is not a primary concern.\n- **Form-based extractors** (`FromForm`): Similar risks to URL extractors, especially if forms are submitted via GET requests.\n- **Header-based extractors** (`FromAuthHeader`, `FromHeader`): Most secure as headers are not typically logged or exposed in referrers.\n- **Cookie-based extractors** (`FromCookie`): Secure for web applications but requires proper cookie security settings (HttpOnly, Secure, SameSite).\n\n**Recommendation**: Use `FromAuthHeader(\"Bearer\")` (the default) for production applications unless you have specific requirements that necessitate alternative extractors.\n\n## Migration from TokenPrefix\n\nIf you were previously using `TokenPrefix`, you can now use `extractors.FromAuthHeader` with the prefix:\n\n```go\n// Old way\npasetoware.New(pasetoware.Config{\n    SymmetricKey: []byte(\"secret\"),\n    TokenPrefix:  \"Bearer\",\n})\n\n// New way\npasetoware.New(pasetoware.Config{\n    SymmetricKey: []byte(\"secret\"),\n    Extractor:    extractors.FromAuthHeader(\"Bearer\"),\n})\n```\n\n## Examples\n\nBelow have a list of some examples that can help you start to use this middleware. In case of any additional example\nthat doesn't show here, please take a look at the test file.\n\n### SymmetricKey\n\n```go\npackage main\n\nimport (\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n\n    pasetoware \"github.com/gofiber/contrib/v3/paseto\"\n)\n\nconst secretSymmetricKey = \"symmetric-secret-key (size = 32)\"\n\nfunc main() {\n\n    app := fiber.New()\n\n    // Login route\n    app.Post(\"/login\", login)\n\n    // Unauthenticated route\n    app.Get(\"/\", accessible)\n\n    // Paseto Middleware with local (encrypted) token\n    apiGroup := app.Group(\"api\", pasetoware.New(pasetoware.Config{\n        SymmetricKey: []byte(secretSymmetricKey),\n        Extractor:    extractors.FromAuthHeader(\"Bearer\"),\n    }))\n\n    // Restricted Routes\n    apiGroup.Get(\"/restricted\", restricted)\n\n    err := app.Listen(\":8088\")\n    if err != nil {\n        return\n    }\n}\n\nfunc login(c fiber.Ctx) error {\n    user := c.FormValue(\"user\")\n    pass := c.FormValue(\"pass\")\n\n    // Throws Unauthorized error\n    if user != \"john\" || pass != \"doe\" {\n        return c.SendStatus(fiber.StatusUnauthorized)\n    }\n\n    // Create token and encrypt it\n    encryptedToken, err := pasetoware.CreateToken([]byte(secretSymmetricKey), user, 12*time.Hour, pasetoware.PurposeLocal)\n    if err != nil {\n        return c.SendStatus(fiber.StatusInternalServerError)\n    }\n\n    return c.JSON(fiber.Map{\"token\": encryptedToken})\n}\n\nfunc accessible(c fiber.Ctx) error {\n    return c.SendString(\"Accessible\")\n}\n\nfunc restricted(c fiber.Ctx) error {\n    payload := pasetoware.FromContext(c).(string)\n    return c.SendString(\"Welcome \" + payload)\n}\n\n```\n\n#### Test it\n\n_Login using username and password to retrieve a token._\n\n```sh\ncurl --data \"user=john&pass=doe\" http://localhost:8088/login\n```\n\n_Response_\n\n```json\n{\n  \"token\": \"<local-token>\"\n}\n```\n\n_Request a restricted resource using the token in Authorization request header._\n\n```sh\ncurl localhost:8088/api/restricted -H \"Authorization: Bearer <local-token>\"\n```\n\n_Response_\n\n```text\nWelcome john\n```\n\n### SymmetricKey + Custom Validator callback\n\n```go\npackage main\n\nimport (\n    \"encoding/json\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n    \"github.com/o1egl/paseto\"\n\n    pasetoware \"github.com/gofiber/contrib/v3/paseto\"\n)\n\nconst secretSymmetricKey = \"symmetric-secret-key (size = 32)\"\n\ntype customPayloadStruct struct {\n    Name      string    `json:\"name\"`\n    ExpiresAt time.Time `json:\"expiresAt\"`\n}\n\nfunc main() {\n\n    app := fiber.New()\n\n    // Login route\n    app.Post(\"/login\", login)\n\n    // Unauthenticated route\n    app.Get(\"/\", accessible)\n\n    // Paseto Middleware with local (encrypted) token\n    apiGroup := app.Group(\"api\", pasetoware.New(pasetoware.Config{\n        SymmetricKey: []byte(secretSymmetricKey),\n        Extractor:    extractors.FromAuthHeader(\"Bearer\"),\n        Validate: func(decrypted []byte) (any, error) {\n            var payload customPayloadStruct\n            err := json.Unmarshal(decrypted, &payload)\n            return payload, err\n        },\n    }))\n\n    // Restricted Routes\n    apiGroup.Get(\"/restricted\", restricted)\n\n    err := app.Listen(\":8088\")\n    if err != nil {\n        return\n    }\n}\n\nfunc login(c fiber.Ctx) error {\n    user := c.FormValue(\"user\")\n    pass := c.FormValue(\"pass\")\n\n    // Throws Unauthorized error\n    if user != \"john\" || pass != \"doe\" {\n        return c.SendStatus(fiber.StatusUnauthorized)\n    }\n\n    // Create the payload\n    payload := customPayloadStruct{\n        Name:      \"John Doe\",\n        ExpiresAt: time.Now().Add(12 * time.Hour),\n    }\n\n    // Create token and encrypt it\n    encryptedToken, err := paseto.NewV2().Encrypt([]byte(secretSymmetricKey), payload, nil)\n    if err != nil {\n        return c.SendStatus(fiber.StatusInternalServerError)\n    }\n\n    return c.JSON(fiber.Map{\"token\": encryptedToken})\n}\n\nfunc accessible(c fiber.Ctx) error {\n    return c.SendString(\"Accessible\")\n}\n\nfunc restricted(c fiber.Ctx) error {\n    payload := pasetoware.FromContext(c).(customPayloadStruct)\n    return c.SendString(\"Welcome \" + payload.Name)\n}\n\n```\n\n### Cookie Extractor Example\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n\n    pasetoware \"github.com/gofiber/contrib/v3/paseto\"\n)\n\nconst secretSymmetricKey = \"symmetric-secret-key (size = 32)\"\n\nfunc main() {\n    app := fiber.New()\n\n    // Paseto Middleware with cookie extractor\n    app.Use(pasetoware.New(pasetoware.Config{\n        SymmetricKey: []byte(secretSymmetricKey),\n        Extractor:    extractors.FromCookie(\"token\"),\n    }))\n\n    app.Get(\"/protected\", func(c fiber.Ctx) error {\n        return c.SendString(\"Protected route\")\n    })\n\n    app.Listen(\":8080\")\n}\n```\n\n### Query Extractor Example\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n\n    pasetoware \"github.com/gofiber/contrib/v3/paseto\"\n)\n\nconst secretSymmetricKey = \"symmetric-secret-key (size = 32)\"\n\nfunc main() {\n    app := fiber.New()\n\n    // Paseto Middleware with query extractor\n    app.Use(pasetoware.New(pasetoware.Config{\n        SymmetricKey: []byte(secretSymmetricKey),\n        Extractor:    extractors.FromQuery(\"token\"),\n    }))\n\n    app.Get(\"/protected\", func(c fiber.Ctx) error {\n        return c.SendString(\"Protected route\")\n    })\n\n    app.Listen(\":8080\")\n}\n```\n\n### PublicPrivate Key\n\n```go\npackage main\n\nimport (\n    \"crypto/ed25519\"\n    \"encoding/hex\"\n    \"time\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/extractors\"\n\n    pasetoware \"github.com/gofiber/contrib/v3/paseto\"\n)\n\nconst privateKeySeed = \"e9c67fe2433aa4110caf029eba70df2c822cad226b6300ead3dcae443ac3810f\"\n\nvar seed, _ = hex.DecodeString(privateKeySeed)\nvar privateKey = ed25519.NewKeyFromSeed(seed)\n\ntype customPayloadStruct struct {\n    Name      string    `json:\"name\"`\n    ExpiresAt time.Time `json:\"expiresAt\"`\n}\n\nfunc main() {\n\n    app := fiber.New()\n\n    // Login route\n    app.Post(\"/login\", login)\n\n    // Unauthenticated route\n    app.Get(\"/\", accessible)\n\n    // Paseto Middleware with public (signed) token\n    apiGroup := app.Group(\"api\", pasetoware.New(pasetoware.Config{\n        Extractor:  extractors.FromAuthHeader(\"Bearer\"),\n        PrivateKey: privateKey,\n        PublicKey:  privateKey.Public(),\n    }))\n\n    // Restricted Routes\n    apiGroup.Get(\"/restricted\", restricted)\n\n    err := app.Listen(\":8088\")\n    if err != nil {\n        return\n    }\n}\n\nfunc login(c fiber.Ctx) error {\n    user := c.FormValue(\"user\")\n    pass := c.FormValue(\"pass\")\n\n    // Throws Unauthorized error\n    if user != \"john\" || pass != \"doe\" {\n        return c.SendStatus(fiber.StatusUnauthorized)\n    }\n\n    // Create token and sign it\n    signedToken, err := pasetoware.CreateToken(privateKey, user, 12*time.Hour, pasetoware.PurposePublic)\n    if err != nil {\n        return c.SendStatus(fiber.StatusInternalServerError)\n    }\n\n    return c.JSON(fiber.Map{\"token\": signedToken})\n}\n\nfunc accessible(c fiber.Ctx) error {\n    return c.SendString(\"Accessible\")\n}\n\nfunc restricted(c fiber.Ctx) error {\n    payload := pasetoware.FromContext(c).(string)\n    return c.SendString(\"Welcome \" + payload)\n}\n\n```\n\n#### Get the payload from the context\n\n```go\npayloadFromCtx := pasetoware.FromContext(c)  \nif payloadFromCtx == nil {  \n    // Handle case where token is not in context, e.g. by returning an error  \n    return  \n}  \npayload := payloadFromCtx.(string)  \n```\n\n`FromContext` accepts a `fiber.Ctx`, `fiber.CustomCtx`, `*fasthttp.RequestCtx`, or a standard `context.Context` (e.g. the value returned by `c.Context()` when `PassLocalsToContext` is enabled):\n\n```go\n// From a fiber.Ctx (most common usage)\npayload := pasetoware.FromContext(c)\n\n// From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled)\npayload := pasetoware.FromContext(c.Context())\n```\n\n#### Test it\n\n_Login using username and password to retrieve a token._\n\n```sh\ncurl --data \"user=john&pass=doe\" http://localhost:8088/login\n```\n\n_Response_\n\n```json\n{\n  \"token\": \"<public-token>\"\n}\n```\n\n_Request a restricted resource using the token in Authorization request header._\n\n```sh\ncurl localhost:8088/api/restricted -H \"Authorization: Bearer <public-token>\"\n```\n\n_Response_\n\n```text\nWelcome John Doe\n```\n"
  },
  {
    "path": "v3/paseto/config.go",
    "content": "package pasetoware\n\nimport (\n\t\"crypto\"\n\t\"crypto/ed25519\"\n\t\"encoding/json\"\n\t\"errors\"\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/o1egl/paseto\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\n// Config defines the config for PASETO 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(fiber.Ctx) bool\n\n\t// SuccessHandler defines a function which is executed for a valid token.\n\t// Optional. Default: c.Next()\n\tSuccessHandler fiber.Handler\n\n\t// ErrorHandler defines a function which is executed for an invalid token.\n\t// It may be used to define a custom PASETO error.\n\t// Optional. Default: 401 Invalid or expired PASETO\n\tErrorHandler fiber.ErrorHandler\n\n\t// Validate defines a function to validate if payload is valid\n\t// Optional. In case payload used is created using CreateToken function\n\t// If token is created using another function, this function must be provided\n\tValidate PayloadValidator\n\n\t// SymmetricKey to validate local tokens.\n\t// If it's set the middleware will use local tokens\n\t//\n\t// Required if PrivateKey and PublicKey are not set\n\tSymmetricKey []byte\n\n\t// PrivateKey to sign public tokens\n\t//\n\t// If it's set the middleware will use public tokens\n\t// Required if SymmetricKey is not set\n\tPrivateKey ed25519.PrivateKey\n\n\t// PublicKey to verify public tokens\n\t//\n\t// If it's set the middleware will use public tokens\n\t// Required if SymmetricKey is not set\n\tPublicKey crypto.PublicKey\n\n\t// Extractor defines a function to extract the token from the request.\n\t// Optional. Default: FromAuthHeader(\"Bearer\").\n\tExtractor extractors.Extractor\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tSuccessHandler: nil,\n\tErrorHandler:   nil,\n\tValidate:       nil,\n\tSymmetricKey:   nil,\n\tExtractor:      extractors.FromAuthHeader(\"Bearer\"),\n}\n\nfunc defaultErrorHandler(c fiber.Ctx, err error) error {\n\t// default to badRequest if error is ErrMissingToken or any paseto decryption error\n\terrorStatus := fiber.StatusBadRequest\n\tif errors.Is(err, ErrDataUnmarshal) || errors.Is(err, ErrExpiredToken) {\n\t\terrorStatus = fiber.StatusUnauthorized\n\t}\n\treturn c.Status(errorStatus).SendString(err.Error())\n}\n\nfunc defaultValidateFunc(data []byte) (interface{}, error) {\n\tvar payload paseto.JSONToken\n\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\treturn nil, ErrDataUnmarshal\n\t}\n\n\tif time.Now().After(payload.Expiration) {\n\t\treturn nil, ErrExpiredToken\n\t}\n\tif err := payload.Validate(\n\t\tpaseto.ValidAt(time.Now()), paseto.Subject(pasetoTokenSubject),\n\t\tpaseto.ForAudience(pasetoTokenAudience),\n\t); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn payload.Get(pasetoTokenField), nil\n}\n\n// Helper function to set default values\nfunc configDefault(authConfigs ...Config) Config {\n\t// Return default authConfigs if nothing provided\n\n\tconfig := ConfigDefault\n\tif len(authConfigs) > 0 {\n\t\t// Override default authConfigs\n\t\tconfig = authConfigs[0]\n\t}\n\n\t// Set default values\n\tif config.SuccessHandler == nil {\n\t\tconfig.SuccessHandler = func(c fiber.Ctx) error {\n\t\t\treturn c.Next()\n\t\t}\n\t}\n\n\tif config.ErrorHandler == nil {\n\t\tconfig.ErrorHandler = defaultErrorHandler\n\t}\n\n\tif config.Validate == nil {\n\t\tconfig.Validate = defaultValidateFunc\n\t}\n\n\tif config.Extractor.Extract == nil {\n\t\tconfig.Extractor = extractors.FromAuthHeader(\"Bearer\")\n\t}\n\n\tif config.SymmetricKey != nil {\n\t\tif len(config.SymmetricKey) != chacha20poly1305.KeySize {\n\t\t\tpanic(\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"Fiber: PASETO middleware requires a symmetric key with size %d\",\n\t\t\t\t\tchacha20poly1305.KeySize,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tif config.PublicKey != nil || config.PrivateKey != nil {\n\t\t\tpanic(\"Fiber: PASETO middleware: can't use PublicKey or PrivateKey with SymmetricKey\")\n\t\t}\n\t} else if config.PublicKey == nil || config.PrivateKey == nil {\n\t\tpanic(\"Fiber: PASETO middleware: need both PublicKey and PrivateKey\")\n\t}\n\n\treturn config\n}\n"
  },
  {
    "path": "v3/paseto/config_test.go",
    "content": "package pasetoware\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n)\n\nfunc assertRecoveryPanic(t *testing.T) {\n\terr := recover()\n\tassert.Equal(t, true, err != nil)\n}\n\nfunc Test_Config_No_SymmetricKey(t *testing.T) {\n\tdefer assertRecoveryPanic(t)\n\tconfig := configDefault()\n\n\tassert.Equal(t, \"\", config.SymmetricKey)\n}\n\nfunc Test_Config_Invalid_SymmetricKey(t *testing.T) {\n\tdefer assertRecoveryPanic(t)\n\tconfig := configDefault()\n\n\tassert.Equal(t, symmetricKey+symmetricKey, config.SymmetricKey)\n}\n\nfunc Test_ConfigDefault(t *testing.T) {\n\tconfig := configDefault(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t})\n\n\tassert.Equal(t, extractors.SourceAuthHeader, config.Extractor.Source)\n\tassert.Equal(t, fiber.HeaderAuthorization, config.Extractor.Key)\n\tassert.Equal(t, \"Bearer\", config.Extractor.AuthScheme)\n\tassert.Empty(t, config.Extractor.Chain)\n\n\tassert.NotNil(t, config.Validate)\n}\n\nfunc Test_ConfigCustomLookup(t *testing.T) {\n\tconfig := configDefault(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tExtractor:    extractors.FromHeader(\"Custom-Header\"),\n\t})\n\tassert.Equal(t, extractors.SourceHeader, config.Extractor.Source)\n\tassert.Equal(t, \"Custom-Header\", config.Extractor.Key)\n\tassert.Equal(t, \"\", config.Extractor.AuthScheme)\n\n\tconfig = configDefault(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tExtractor:    extractors.FromQuery(\"token\"),\n\t})\n\tassert.Equal(t, extractors.SourceQuery, config.Extractor.Source)\n\tassert.Equal(t, \"token\", config.Extractor.Key)\n\tassert.Equal(t, \"\", config.Extractor.AuthScheme)\n}\n"
  },
  {
    "path": "v3/paseto/go.mod",
    "content": "module github.com/gofiber/contrib/v3/paseto\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/o1egl/paseto v1.0.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/crypto v0.50.0\n)\n\nrequire (\n\tgithub.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect\n\tgithub.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect\n\tgithub.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/paseto/go.sum",
    "content": "github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=\ngithub.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=\ngithub.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU=\ngithub.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE=\ngithub.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU=\ngithub.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=\ngithub.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=\ngithub.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0=\ngithub.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/paseto/helpers.go",
    "content": "package pasetoware\n\nimport (\n\t\"crypto/ed25519\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/o1egl/paseto\"\n)\n\ntype TokenPurpose int\n\nconst (\n\tPurposeLocal TokenPurpose = iota\n\tPurposePublic\n)\n\nvar (\n\tErrExpiredToken  = errors.New(\"token has expired\")\n\tErrMissingToken  = errors.New(\"missing PASETO token\")\n\tErrDataUnmarshal = errors.New(\"can't unmarshal token data to Payload type\")\n\tpasetoObject     = paseto.NewV2()\n)\n\n// PayloadValidator Function that receives the decrypted payload and returns an interface and an error\n// that's a result of validation logic\ntype PayloadValidator func(decrypted []byte) (interface{}, error)\n\n// PayloadCreator Signature of a function that generates a payload token\ntype PayloadCreator func(key []byte, dataInfo string, duration time.Duration, purpose TokenPurpose) (string, error)\n\n// Public helper functions\n\n// CreateToken Create a new Token Payload that will be stored in PASETO\nfunc CreateToken(key []byte, dataInfo string, duration time.Duration, purpose TokenPurpose) (string, error) {\n\tpayload, err := NewPayload(dataInfo, duration)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tswitch purpose {\n\tcase PurposeLocal:\n\t\treturn pasetoObject.Encrypt(key, payload, nil)\n\tcase PurposePublic:\n\t\treturn pasetoObject.Sign(ed25519.PrivateKey(key), payload, nil)\n\tdefault:\n\t\treturn pasetoObject.Encrypt(key, payload, nil)\n\t}\n}\n"
  },
  {
    "path": "v3/paseto/paseto.go",
    "content": "package pasetoware\n\nimport (\n\t\"errors\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\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 following contextKey values are defined to store values in context.\nconst (\n\tpayloadKey contextKey = iota\n)\n\n// New PASETO middleware returns a handler that takes a token in the selected lookup param and,\n// when valid, stores the decrypted payload via fiber.StoreInContext (locals + context when enabled).\n// See Config for more configuration options.\nfunc New(authConfigs ...Config) fiber.Handler {\n\t// Set default authConfig\n\tconfig := configDefault(authConfigs...)\n\n\t// Return middleware handler\n\treturn func(c fiber.Ctx) error {\n\t\t// Filter request to skip middleware\n\t\tif config.Next != nil && config.Next(c) {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\ttoken, err := config.Extractor.Extract(c)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, extractors.ErrNotFound) {\n\t\t\t\treturn config.ErrorHandler(c, ErrMissingToken)\n\t\t\t}\n\t\t\treturn config.ErrorHandler(c, err)\n\t\t}\n\n\t\tvar outData []byte\n\n\t\tif config.SymmetricKey != nil {\n\t\t\tif err := pasetoObject.Decrypt(token, config.SymmetricKey, &outData, nil); err != nil {\n\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := pasetoObject.Verify(token, config.PublicKey, &outData, nil); err != nil {\n\t\t\t\treturn config.ErrorHandler(c, err)\n\t\t\t}\n\t\t}\n\n\t\tpayload, err := config.Validate(outData)\n\t\tif err == nil {\n\t\t\t// Store user information from token into context.\n\t\t\tfiber.StoreInContext(c, payloadKey, payload)\n\n\t\t\treturn config.SuccessHandler(c)\n\t\t}\n\n\t\treturn config.ErrorHandler(c, err)\n\t}\n}\n\n// FromContext returns the payload from the context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\nfunc FromContext(ctx any) interface{} {\n\tpayload, _ := fiber.ValueFromContext[interface{}](ctx, payloadKey)\n\treturn payload\n}\n"
  },
  {
    "path": "v3/paseto/paseto_test.go",
    "content": "package pasetoware\n\nimport (\n\t\"crypto/ed25519\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/extractors\"\n)\n\nconst (\n\ttestMessage    = \"fiber with PASETO middleware!!\"\n\tinvalidToken   = \"We are gophers!\"\n\tdurationTest   = 10 * time.Minute\n\tsymmetricKey   = \"go+fiber=love;FiberWithPASETO<3!\"\n\tprivateKeySeed = \"e9c67fe2433aa4110caf029eba70df2c822cad226b6300ead3dcae443ac3810f\"\n)\n\ntype customPayload struct {\n\tData           string        `json:\"data\"`\n\tExpirationTime time.Duration `json:\"expiration_time\"`\n\tCreatedAt      time.Time     `json:\"created_at\"`\n}\n\nfunc createCustomToken(key []byte, dataInfo string, duration time.Duration, purpose TokenPurpose) (string, error) {\n\tif purpose == PurposeLocal {\n\t\treturn pasetoObject.Encrypt(key, customPayload{\n\t\t\tData:           dataInfo,\n\t\t\tExpirationTime: duration,\n\t\t\tCreatedAt:      time.Now(),\n\t\t}, nil)\n\t}\n\n\treturn pasetoObject.Sign(ed25519.PrivateKey(key), customPayload{\n\t\tData:           dataInfo,\n\t\tExpirationTime: duration,\n\t\tCreatedAt:      time.Now(),\n\t}, nil)\n}\n\nfunc generateTokenRequest(\n\ttargetRoute string, tokenGenerator PayloadCreator, duration time.Duration, purpose TokenPurpose,\n\tschemes ...string,\n) (*http.Request, error) {\n\tvar token string\n\tvar err error\n\tif purpose == PurposeLocal {\n\t\ttoken, err = tokenGenerator([]byte(symmetricKey), testMessage, duration, purpose)\n\t} else {\n\t\tseed, _ := hex.DecodeString(privateKeySeed)\n\t\tprivateKey := ed25519.NewKeyFromSeed(seed)\n\n\t\ttoken, err = tokenGenerator(privateKey, testMessage, duration, purpose)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trequest := httptest.NewRequest(\"GET\", targetRoute, nil)\n\tscheme := \"Bearer\"\n\tif len(schemes) > 0 && schemes[0] != \"\" {\n\t\tscheme = schemes[0]\n\t}\n\trequest.Header.Set(fiber.HeaderAuthorization, scheme+\" \"+token)\n\treturn request, nil\n}\n\nfunc getPrivateKey() ed25519.PrivateKey {\n\tseed, _ := hex.DecodeString(privateKeySeed)\n\n\treturn ed25519.NewKeyFromSeed(seed)\n}\n\nfunc assertErrorHandler(t *testing.T, toAssert error) fiber.ErrorHandler {\n\tt.Helper()\n\treturn func(ctx fiber.Ctx, err error) error {\n\t\tassert.Equal(t, toAssert, err)\n\t\tassert.Equal(t, true, errors.Is(err, toAssert))\n\t\treturn defaultErrorHandler(ctx, err)\n\t}\n}\n\nfunc Test_PASETO_LocalToken_MissingToken(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tErrorHandler: assertErrorHandler(t, ErrMissingToken),\n\t}))\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\tresp, err := app.Test(request)\n\tif err == nil {\n\t\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\t}\n}\n\nfunc Test_PASETO_PublicToken_MissingToken(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey:   privateKey,\n\t\tPublicKey:    privateKey.Public(),\n\t\tErrorHandler: assertErrorHandler(t, ErrMissingToken),\n\t}))\n\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalToken_ErrDataUnmarshal(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tErrorHandler: assertErrorHandler(t, ErrDataUnmarshal),\n\t}))\n\trequest, err := generateTokenRequest(\"/\", createCustomToken, durationTest, PurposeLocal)\n\tif err == nil {\n\t\tvar resp *http.Response\n\t\tresp, err = app.Test(request)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\t}\n}\n\nfunc Test_PASETO_PublicToken_ErrDataUnmarshal(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\tPublicKey:  privateKey.Public(),\n\t}))\n\n\trequest, err := generateTokenRequest(\"/\", createCustomToken, durationTest, PurposePublic)\n\n\tassert.Equal(t, nil, err)\n\n\tvar resp *http.Response\n\tresp, err = app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalToken_ErrTokenExpired(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tErrorHandler: assertErrorHandler(t, ErrExpiredToken),\n\t}))\n\trequest, err := generateTokenRequest(\"/\", CreateToken, time.Nanosecond*-10, PurposeLocal)\n\tif err == nil {\n\t\tvar resp *http.Response\n\t\tresp, err = app.Test(request)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n\t}\n}\n\nfunc Test_PASETO_PublicToken_ErrTokenExpired(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey:   privateKey,\n\t\tPublicKey:    privateKey.Public(),\n\t\tErrorHandler: assertErrorHandler(t, ErrExpiredToken),\n\t}))\n\n\trequest, err := generateTokenRequest(\"/\", CreateToken, time.Nanosecond*-10, PurposePublic)\n\n\tassert.Equal(t, nil, err)\n\n\tvar resp *http.Response\n\tresp, err = app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalToken_Next(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_PASETO_PublicToken_Next(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\tPublicKey:  privateKey.Public(),\n\t\tNext: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t}))\n\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalTokenDecrypt(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t}))\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tassert.Equal(t, testMessage, FromContext(ctx))\n\t\treturn nil\n\t})\n\trequest, err := generateTokenRequest(\"/\", CreateToken, durationTest, PurposeLocal)\n\tif err == nil {\n\t\tvar resp *http.Response\n\t\tresp, err = app.Test(request)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\t}\n}\n\nfunc Test_PASETO_PublicTokenVerify(t *testing.T) {\n\tseed, _ := hex.DecodeString(privateKeySeed)\n\tprivateKey := ed25519.NewKeyFromSeed(seed)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\tPublicKey:  privateKey.Public(),\n\t}))\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tassert.Equal(t, testMessage, FromContext(ctx))\n\t\treturn nil\n\t})\n\trequest, err := generateTokenRequest(\"/\", CreateToken, durationTest, PurposePublic)\n\tassert.Equal(t, nil, err)\n\n\tvar resp *http.Response\n\tresp, err = app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalToken_IncorrectBearerToken(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tExtractor:    extractors.FromAuthHeader(\"Gopher\"),\n\t}))\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PASETO_PublicToken_IncorrectBearerToken(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\tPublicKey:  privateKey.Public(),\n\t\tExtractor:  extractors.FromAuthHeader(\"Gopher\"),\n\t}))\n\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalToken_InvalidToken(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t}))\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PASETO_PublicToken_InvalidToken(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\tPublicKey:  privateKey.Public(),\n\t}))\n\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n}\n\nfunc Test_PASETO_LocalToken_CustomValidate(t *testing.T) {\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tValidate: func(data []byte) (interface{}, error) {\n\t\t\tvar payload customPayload\n\t\t\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\t\t\treturn nil, ErrDataUnmarshal\n\t\t\t}\n\n\t\t\tif time.Now().After(payload.CreatedAt.Add(payload.ExpirationTime)) {\n\t\t\t\treturn nil, ErrExpiredToken\n\t\t\t}\n\t\t\treturn payload.Data, nil\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tassert.Equal(t, testMessage, FromContext(ctx))\n\t\treturn nil\n\t})\n\n\ttoken, _ := pasetoObject.Encrypt([]byte(symmetricKey), customPayload{\n\t\tData:           testMessage,\n\t\tExpirationTime: 10 * time.Minute,\n\t\tCreatedAt:      time.Now(),\n\t}, nil)\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+token)\n\tresp, err := app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_PASETO_PublicToken_CustomValidate(t *testing.T) {\n\tprivateKey := getPrivateKey()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\tPublicKey:  privateKey.Public(),\n\t\tValidate: func(data []byte) (interface{}, error) {\n\t\t\tvar payload customPayload\n\t\t\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\t\t\treturn nil, ErrDataUnmarshal\n\t\t\t}\n\n\t\t\tif time.Now().After(payload.CreatedAt.Add(payload.ExpirationTime)) {\n\t\t\t\treturn nil, ErrExpiredToken\n\t\t\t}\n\t\t\treturn payload.Data, nil\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tassert.Equal(t, testMessage, FromContext(ctx))\n\t\treturn nil\n\t})\n\n\ttoken, _ := pasetoObject.Sign(privateKey, customPayload{\n\t\tData:           testMessage,\n\t\tExpirationTime: 10 * time.Minute,\n\t\tCreatedAt:      time.Now(),\n\t}, nil)\n\n\trequest := httptest.NewRequest(\"GET\", \"/\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+token)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_PASETO_CustomErrorHandler(t *testing.T) {\n\tapp := fiber.New()\n\n\tcustomErrorCalled := false\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tErrorHandler: func(ctx fiber.Ctx, err error) error {\n\t\t\tcustomErrorCalled = true\n\t\t\treturn ctx.Status(fiber.StatusTeapot).SendString(\"Custom PASETO Error: \" + err.Error())\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(\"OK\")\n\t})\n\n\trequest := httptest.NewRequest(\"GET\", \"/protected\", nil)\n\trequest.Header.Set(fiber.HeaderAuthorization, \"Bearer \"+invalidToken)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusTeapot, resp.StatusCode)\n\tassert.True(t, customErrorCalled)\n}\n\nfunc Test_PASETO_CustomSuccessHandler(t *testing.T) {\n\tapp := fiber.New()\n\n\tcustomSuccessCalled := false\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tSuccessHandler: func(ctx fiber.Ctx) error {\n\t\t\tcustomSuccessCalled = true\n\t\t\tctx.Locals(\"custom\", \"paseto-success\")\n\t\t\treturn ctx.Next()\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\tif ctx.Locals(\"custom\") == \"paseto-success\" {\n\t\t\treturn ctx.SendString(\"Custom Success Handler Worked\")\n\t\t}\n\t\treturn ctx.SendString(\"OK\")\n\t})\n\n\trequest, err := generateTokenRequest(\"/protected\", CreateToken, durationTest, PurposeLocal)\n\tassert.Equal(t, nil, err)\n\n\tresp, err := app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tassert.True(t, customSuccessCalled)\n}\n\nfunc Test_PASETO_InvalidSymmetricKey(t *testing.T) {\n\tdefer func() {\n\t\terr := recover()\n\t\tassert.NotNil(t, err)\n\t}()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(\"invalid-key-length\"), // Wrong length\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(\"OK\")\n\t})\n}\n\nfunc Test_PASETO_MissingPublicKey(t *testing.T) {\n\tdefer func() {\n\t\terr := recover()\n\t\tassert.NotNil(t, err)\n\t}()\n\n\tprivateKey := getPrivateKey()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPrivateKey: privateKey,\n\t\t// Missing PublicKey\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(\"OK\")\n\t})\n}\n\nfunc Test_PASETO_MissingPrivateKey(t *testing.T) {\n\tdefer func() {\n\t\terr := recover()\n\t\tassert.NotNil(t, err)\n\t}()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tPublicKey: getPrivateKey().Public(),\n\t\t// Missing PrivateKey\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(\"OK\")\n\t})\n}\n\nfunc Test_PASETO_BothKeysProvided(t *testing.T) {\n\tdefer func() {\n\t\terr := recover()\n\t\tassert.NotNil(t, err)\n\t}()\n\n\tprivateKey := getPrivateKey()\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tPrivateKey:   privateKey,\n\t\tPublicKey:    privateKey.Public(),\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(\"OK\")\n\t})\n}\n\nfunc Test_PASETO_FromContextWithoutToken(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Get(\"/no-token\", func(ctx fiber.Ctx) error {\n\t\tpayload := FromContext(ctx)\n\t\tif payload == nil {\n\t\t\treturn ctx.SendString(\"No payload as expected\")\n\t\t}\n\t\treturn ctx.SendString(\"Unexpected payload\")\n\t})\n\n\trequest := httptest.NewRequest(\"GET\", \"/no-token\", nil)\n\tresp, err := app.Test(request)\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n\nfunc Test_PASETO_CustomValidateError(t *testing.T) {\n\tapp := fiber.New()\n\n\tapp.Use(New(Config{\n\t\tSymmetricKey: []byte(symmetricKey),\n\t\tValidate: func(data []byte) (interface{}, error) {\n\t\t\treturn nil, fiber.NewError(fiber.StatusForbidden, \"Custom validation failed\")\n\t\t},\n\t\tErrorHandler: func(ctx fiber.Ctx, err error) error {\n\t\t\treturn ctx.Status(fiber.StatusForbidden).SendString(\"Validation failed\")\n\t\t},\n\t}))\n\n\tapp.Get(\"/protected\", func(ctx fiber.Ctx) error {\n\t\treturn ctx.SendString(\"OK\")\n\t})\n\n\trequest, err := generateTokenRequest(\"/protected\", CreateToken, durationTest, PurposeLocal)\n\tassert.Equal(t, nil, err)\n\n\tresp, err := app.Test(request)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusForbidden, resp.StatusCode)\n}\n\nfunc Test_PASETO_FromContext_PassLocalsToContext(t *testing.T) {\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\n\tapp.Use(New(Config{SymmetricKey: []byte(symmetricKey)}))\n\tapp.Get(\"/\", func(ctx fiber.Ctx) error {\n\t\tpayload := FromContext(ctx)\n\t\tpayloadFromContext := FromContext(ctx.Context())\n\t\tif payload == nil || payloadFromContext == nil {\n\t\t\treturn ctx.SendStatus(fiber.StatusUnauthorized)\n\t\t}\n\t\treturn ctx.SendStatus(fiber.StatusOK)\n\t})\n\n\trequest, err := generateTokenRequest(\"/\", CreateToken, durationTest, PurposeLocal)\n\tassert.NoError(t, err)\n\n\tresp, err := app.Test(request)\n\tassert.NoError(t, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n}\n"
  },
  {
    "path": "v3/paseto/payload.go",
    "content": "package pasetoware\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/o1egl/paseto\"\n)\n\nconst (\n\tpasetoTokenAudience = \"gofiber.gophers\"\n\tpasetoTokenSubject  = \"user-token\"\n\tpasetoTokenField    = \"data\"\n)\n\n// NewPayload generates a new paseto.JSONToken and returns it and a error that can be caused by uuid\nfunc NewPayload(userToken string, duration time.Duration) (*paseto.JSONToken, error) {\n\ttokenID, err := uuid.NewRandom()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttimeNow := time.Now()\n\tpayload := &paseto.JSONToken{\n\t\tAudience:   pasetoTokenAudience,\n\t\tJti:        tokenID.String(),\n\t\tSubject:    pasetoTokenSubject,\n\t\tIssuedAt:   timeNow,\n\t\tExpiration: timeNow.Add(duration),\n\t\tNotBefore:  timeNow,\n\t}\n\n\tpayload.Set(pasetoTokenField, userToken)\n\treturn payload, nil\n}\n"
  },
  {
    "path": "v3/sentry/README.md",
    "content": "---\nid: sentry\n---\n\n# Sentry\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*sentry*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20sentry/badge.svg)\n\n[Sentry](https://sentry.io/) support for Fiber.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/sentry\ngo get -u github.com/getsentry/sentry-go\n```\n\n## Signature\n\n```go\nfiberSentry.New(config ...fiberSentry.Config) fiber.Handler\nfiberSentry.GetHubFromContext(ctx any) *sdk.Hub         // sdk \"github.com/getsentry/sentry-go\"\nfiberSentry.MustGetHubFromContext(ctx any) *sdk.Hub     // sdk \"github.com/getsentry/sentry-go\"\n```\n\n`GetHubFromContext` and `MustGetHubFromContext` each accept a `fiber.Ctx`, `fiber.CustomCtx`, `*fasthttp.RequestCtx`, or a standard `context.Context` (e.g. the value returned by `c.Context()` when `PassLocalsToContext` is enabled). The `Must*` variant panics if the hub is not found. `*sdk.Hub` is `*sentry.Hub` from `github.com/getsentry/sentry-go`.\n\n## Config\n\n| Property        | Type            | Description                                                                                                                                                                                                                                                          | Default           |\n| :-------------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------- |\n| Repanic         | `bool`          | Repanic configures whether Sentry should repanic after recovery. Set to true, if [Recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) middleware is used.                                                                                      | `false`           |\n| WaitForDelivery | `bool`          | WaitForDelivery configures whether you want to block the request before moving forward with the response. If [Recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) middleware is used, it's safe to either skip this option or set it to false. | `false`           |\n| Timeout         | `time.Duration` | Timeout for the event delivery requests.                                                                                                                                                                                                                             | `time.Second * 2` |\n\n## Usage\n\n`sentry` attaches an instance of `*sentry.Hub` (https://godoc.org/github.com/getsentry/sentry-go#Hub) to the request's context, which makes it available throughout the rest of the request's lifetime.\nYou can access it by using the `sentry.GetHubFromContext()` or `sentry.MustGetHubFromContext()` method on the context itself in any of your proceeding middleware and routes.\nKeep in mind that `*sentry.Hub` should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests.\n\n- **Keep in mind that `*sentry.Hub` won't be available in middleware attached before `sentry`. In this case, `GetHubFromContext()` returns nil, and `MustGetHubFromContext()` will panic.**\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n\n    sdk \"github.com/getsentry/sentry-go\"\n    fiberSentry \"github.com/gofiber/contrib/v3/sentry\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/utils\"\n)\n\nfunc main() {\n    _ = sdk.Init(sdk.ClientOptions{\n        Dsn: \"\",\n        BeforeSend: func(event *sdk.Event, hint *sdk.EventHint) *sdk.Event {\n            if hint.Context != nil {\n                if c, ok := hint.Context.Value(sdk.RequestContextKey).(fiber.Ctx); ok {\n                    // You have access to the original Context if it panicked\n                    fmt.Println(utils.ImmutableString(c.Hostname()))\n                }\n            }\n            fmt.Println(event)\n            return event\n        },\n        Debug:            true,\n        AttachStacktrace: true,\n    })\n\n    app := fiber.New()\n\n    app.Use(fiberSentry.New(fiberSentry.Config{\n        Repanic:         true,\n        WaitForDelivery: true,\n    }))\n\n    enhanceSentryEvent := func(c fiber.Ctx) error {\n        if hub := fiberSentry.GetHubFromContext(c); hub != nil {\n            hub.Scope().SetTag(\"someRandomTag\", \"maybeYouNeedIt\")\n        }\n        return c.Next()\n    }\n\n    app.All(\"/foo\", enhanceSentryEvent, func(c fiber.Ctx) error {\n        panic(\"y tho\")\n    })\n\n    app.All(\"/\", func(c fiber.Ctx) error {\n        if hub := fiberSentry.GetHubFromContext(c); hub != nil {\n            hub.WithScope(func(scope *sdk.Scope) {\n                scope.SetExtra(\"unwantedQuery\", \"someQueryDataMaybe\")\n                hub.CaptureMessage(\"User provided unwanted query string, but we recovered just fine\")\n            })\n        }\n        return c.SendStatus(fiber.StatusOK)\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## Accessing Context in `BeforeSend` callback\n\n```go\nimport (\n    \"fmt\"\n\n    \"github.com/gofiber/fiber/v3\"\n    sdk \"github.com/getsentry/sentry-go\"\n)\n\nsdk.Init(sdk.ClientOptions{\n    Dsn: \"your-public-dsn\",\n    BeforeSend: func(event *sdk.Event, hint *sdk.EventHint) *sdk.Event {\n        if hint.Context != nil {\n            if c, ok := hint.Context.Value(sdk.RequestContextKey).(fiber.Ctx); ok {\n                // You have access to the original Context if it panicked\n                fmt.Println(c.Hostname())\n            }\n        }\n        return event\n    },\n})\n```\n\n## Retrieving the hub with PassLocalsToContext\n\nWhen `fiber.Config{PassLocalsToContext: true}` is set, the Sentry hub stored by the middleware is also available in the underlying `context.Context`. Use `GetHubFromContext` or `MustGetHubFromContext` with any of the supported context types:\n\n```go\n// From a fiber.Ctx (most common usage)\nhub := fiberSentry.GetHubFromContext(c)\n\n// From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled)\nhub := fiberSentry.GetHubFromContext(c.Context())\n```\n\n`MustGetHubFromContext` panics if the hub is not found (e.g. in middleware that runs before `sentry`):\n\n```go\nhub := fiberSentry.MustGetHubFromContext(c)\n```\n"
  },
  {
    "path": "v3/sentry/config.go",
    "content": "package sentry\n\nimport \"time\"\n\n// The contextKey type is unexported to prevent collisions with context keys defined in\n// other packages.\ntype contextKey int\n\nconst (\n\thubKey contextKey = iota\n)\n\n// Config defines the config for middleware.\ntype Config struct {\n\t// Repanic configures whether Sentry should repanic after recovery.\n\t// Set to true, if Recover middleware is used.\n\t// https://github.com/gofiber/fiber/tree/master/middleware/recover\n\t// Optional. Default: false\n\tRepanic bool\n\n\t// WaitForDelivery configures whether you want to block the request before moving forward with the response.\n\t// If Recover middleware is used, it's safe to either skip this option or set it to false.\n\t// https://github.com/gofiber/fiber/tree/master/middleware/recover\n\t// Optional. Default: false\n\tWaitForDelivery bool\n\n\t// Timeout for the event delivery requests.\n\t// Optional. Default: 2 Seconds\n\tTimeout time.Duration\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tRepanic:         false,\n\tWaitForDelivery: false,\n\tTimeout:         time.Second * 2,\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.Timeout == 0 {\n\t\tcfg.Timeout = time.Second * 2\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/sentry/go.mod",
    "content": "module github.com/gofiber/contrib/v3/sentry\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/getsentry/sentry-go v0.45.1\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n)\n"
  },
  {
    "path": "v3/sentry/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/getsentry/sentry-go v0.45.1 h1:9rfzJtGiJG+MGIaWZXidDGHcH5GU1Z5y0WVJGf9nysw=\ngithub.com/getsentry/sentry-go v0.45.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=\ngithub.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=\ngithub.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=\ngithub.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/sentry/sentry.go",
    "content": "package sentry\n\nimport (\n\t\"context\"\n\n\t\"github.com/getsentry/sentry-go\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/adaptor\"\n\t\"github.com/gofiber/utils/v2\"\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// Convert fiber request to http request\n\t\tr, err := adaptor.ConvertRequest(c, true)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Init sentry hub\n\t\thub := sentry.CurrentHub().Clone()\n\t\tscope := hub.Scope()\n\t\tscope.SetRequest(r)\n\t\tscope.SetRequestBody(utils.CopyBytes(c.Body()))\n\t\tfiber.StoreInContext(c, hubKey, hub)\n\n\t\t// Catch panics\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\teventID := hub.RecoverWithContext(\n\t\t\t\t\tcontext.WithValue(context.Background(), sentry.RequestContextKey, c),\n\t\t\t\t\terr,\n\t\t\t\t)\n\n\t\t\t\tif eventID != nil && cfg.WaitForDelivery {\n\t\t\t\t\thub.Flush(cfg.Timeout)\n\t\t\t\t}\n\n\t\t\t\tif cfg.Repanic {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\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\n// MustGetHubFromContext returns the Sentry hub from context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\n// Panics if the hub is not found or has an unexpected type.\nfunc MustGetHubFromContext(ctx any) *sentry.Hub {\n\thub := GetHubFromContext(ctx)\n\tif hub == nil {\n\t\tpanic(\"sentry: hub not found in context or has unexpected type\")\n\t}\n\n\treturn hub\n}\n\n// GetHubFromContext returns the Sentry hub from context.\n// It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context.\nfunc GetHubFromContext(ctx any) *sentry.Hub {\n\thub, ok := fiber.ValueFromContext[*sentry.Hub](ctx, hubKey)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn hub\n}\n"
  },
  {
    "path": "v3/sentry/sentry_test.go",
    "content": "package sentry\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/getsentry/sentry-go\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testCase struct {\n\tdesc    string\n\tpath    string\n\tmethod  string\n\tbody    string\n\thandler fiber.Handler\n\tevent   *sentry.Event\n}\n\nfunc testCasesBeforeRegister(t *testing.T) []testCase {\n\treturn []testCase{\n\t\t{\n\t\t\tdesc:   \"MustGetHubFromContext without Sentry middleware\",\n\t\t\tpath:   \"/no-middleware\",\n\t\t\tmethod: \"GET\",\n\t\t\thandler: func(c fiber.Ctx) error {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Fatal(\"MustGetHubFromContext did not panic\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\t_ = MustGetHubFromContext(c) // This should panic\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tevent: nil, // No event expected because a panic should occur\n\t\t},\n\t\t{\n\t\t\tdesc:   \"GetHubFromContext without Sentry middleware\",\n\t\t\tpath:   \"/no-middleware-2\",\n\t\t\tmethod: \"GET\",\n\t\t\thandler: func(c fiber.Ctx) error {\n\t\t\t\thub := GetHubFromContext(c)\n\t\t\t\tif hub != nil {\n\t\t\t\t\tt.Fatal(\"Expected nil, got a Sentry hub instance\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tevent: nil, // No Sentry event expected here\n\t\t},\n\t}\n}\n\nvar testCasesAfterRegister = []testCase{\n\t{\n\t\tdesc:   \"panic\",\n\t\tpath:   \"/panic\",\n\t\tmethod: \"GET\",\n\t\thandler: func(c fiber.Ctx) error {\n\t\t\tpanic(\"test\")\n\t\t},\n\t\tevent: &sentry.Event{\n\t\t\tLevel:   sentry.LevelFatal,\n\t\t\tMessage: \"test\",\n\t\t\tRequest: &sentry.Request{\n\t\t\t\tURL:    \"http://example.com/panic\",\n\t\t\t\tMethod: \"GET\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Host\":       \"example.com\",\n\t\t\t\t\t\"User-Agent\": \"fiber\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tdesc:   \"post\",\n\t\tpath:   \"/post\",\n\t\tmethod: \"POST\",\n\t\tbody:   \"payload\",\n\t\thandler: func(c fiber.Ctx) error {\n\t\t\thub := MustGetHubFromContext(c)\n\t\t\thub.CaptureMessage(\"post: \" + string(c.Body()))\n\t\t\treturn nil\n\t\t},\n\t\tevent: &sentry.Event{\n\t\t\tLevel:   sentry.LevelInfo,\n\t\t\tMessage: \"post: payload\",\n\t\t\tRequest: &sentry.Request{\n\t\t\t\tURL:    \"http://example.com/post\",\n\t\t\t\tMethod: \"POST\",\n\t\t\t\tData:   \"payload\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Content-Length\": \"7\",\n\t\t\t\t\t\"Host\":           \"example.com\",\n\t\t\t\t\t\"User-Agent\":     \"fiber\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tdesc:   \"get\",\n\t\tpath:   \"/get\",\n\t\tmethod: \"GET\",\n\t\thandler: func(c fiber.Ctx) error {\n\t\t\thub := MustGetHubFromContext(c)\n\t\t\thub.CaptureMessage(\"get\")\n\t\t\treturn nil\n\t\t},\n\t\tevent: &sentry.Event{\n\t\t\tLevel:   sentry.LevelInfo,\n\t\t\tMessage: \"get\",\n\t\t\tRequest: &sentry.Request{\n\t\t\t\tURL:    \"http://example.com/get\",\n\t\t\t\tMethod: \"GET\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Host\":       \"example.com\",\n\t\t\t\t\t\"User-Agent\": \"fiber\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tdesc:   \"large body\",\n\t\tpath:   \"/post/large\",\n\t\tmethod: \"POST\",\n\t\tbody:   strings.Repeat(\"Large\", 3*1024), // 15 KB\n\t\thandler: func(c fiber.Ctx) error {\n\t\t\thub := MustGetHubFromContext(c)\n\t\t\thub.CaptureMessage(fmt.Sprintf(\"post: %d KB\", len(c.Body())/1024))\n\t\t\treturn nil\n\t\t},\n\t\tevent: &sentry.Event{\n\t\t\tLevel:   sentry.LevelInfo,\n\t\t\tMessage: \"post: 15 KB\",\n\t\t\tRequest: &sentry.Request{\n\t\t\t\tURL:    \"http://example.com/post/large\",\n\t\t\t\tMethod: \"POST\",\n\t\t\t\t// Actual request body omitted because too large.\n\t\t\t\tData: \"\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Content-Length\": \"15360\",\n\t\t\t\t\t\"Host\":           \"example.com\",\n\t\t\t\t\t\"User-Agent\":     \"fiber\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tdesc:   \"ignore body\",\n\t\tpath:   \"/post/body-ignored\",\n\t\tmethod: \"POST\",\n\t\tbody:   \"client sends, fasthttp always reads, SDK reports\",\n\t\thandler: func(c fiber.Ctx) error {\n\t\t\thub := MustGetHubFromContext(c)\n\t\t\thub.CaptureMessage(\"body ignored\")\n\t\t\treturn nil\n\t\t},\n\t\tevent: &sentry.Event{\n\t\t\tLevel:   sentry.LevelInfo,\n\t\t\tMessage: \"body ignored\",\n\t\t\tRequest: &sentry.Request{\n\t\t\t\tURL:    \"http://example.com/post/body-ignored\",\n\t\t\t\tMethod: \"POST\",\n\t\t\t\t// Actual request body included because fasthttp always\n\t\t\t\t// reads full request body.\n\t\t\t\tData: \"client sends, fasthttp always reads, SDK reports\",\n\t\t\t\tHeaders: map[string]string{\n\t\t\t\t\t\"Content-Length\": \"48\",\n\t\t\t\t\t\"Host\":           \"example.com\",\n\t\t\t\t\t\"User-Agent\":     \"fiber\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc Test_Sentry(t *testing.T) {\n\tapp := fiber.New()\n\n\ttestFunc := func(t *testing.T, tC testCase) {\n\t\tt.Run(tC.desc, func(t *testing.T) {\n\t\t\tif err := sentry.Init(sentry.ClientOptions{\n\t\t\t\tBeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {\n\t\t\t\t\trequire.Equal(t, tC.event.Message, event.Message)\n\t\t\t\t\trequire.Equal(t, tC.event.Request, event.Request)\n\t\t\t\t\trequire.Equal(t, tC.event.Level, event.Level)\n\t\t\t\t\trequire.Equal(t, tC.event.Exception, event.Exception)\n\t\t\t\t\treturn event\n\t\t\t\t},\n\t\t\t}); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tapp.Add([]string{tC.method}, tC.path, tC.handler)\n\n\t\t\treq, err := http.NewRequest(tC.method, \"http://example.com\"+tC.path, strings.NewReader(tC.body))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treq.Header.Set(\"User-Agent\", \"fiber\")\n\n\t\t\tresp, err := app.Test(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Request %q failed: %s\", tC.path, err)\n\t\t\t}\n\t\t\tresp.Body.Close()\n\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\tt.Fatalf(\"Status code = %d\", resp.StatusCode)\n\t\t\t}\n\t\t})\n\t}\n\n\tfor _, tC := range testCasesBeforeRegister(t) {\n\t\ttestFunc(t, tC)\n\t}\n\n\tapp.Use(New())\n\n\tfor _, tC := range testCasesAfterRegister {\n\t\ttestFunc(t, tC)\n\t}\n\n\tif ok := sentry.Flush(time.Second); !ok {\n\t\tt.Fatal(\"sentry.Flush timed out\")\n\t}\n}\n\nfunc Test_GetHubFromContext_PassLocalsToContext(t *testing.T) {\n\tapp := fiber.New(fiber.Config{PassLocalsToContext: true})\n\tapp.Use(New())\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\thub := GetHubFromContext(c)\n\t\thubFromContext := GetHubFromContext(c.Context())\n\t\trequire.NotNil(t, hub)\n\t\trequire.NotNil(t, hubFromContext)\n\t\treturn c.SendStatus(http.StatusOK)\n\t})\n\n\treq, err := http.NewRequest(http.MethodGet, \"http://example.com/\", nil)\n\trequire.NoError(t, err)\n\tresp, err := app.Test(req)\n\trequire.NoError(t, err)\n\trequire.Equal(t, http.StatusOK, resp.StatusCode)\n}\n"
  },
  {
    "path": "v3/socketio/README.md",
    "content": "---\nid: socketio\n---\n\n# Socket.io\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*socketio*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20Socket.io/badge.svg)\n\nWebSocket wrapper for [Fiber](https://github.com/gofiber/fiber) with events support and inspired by [Socket.io](https://github.com/socketio/socket.io)\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/socketio\n```\n\n## Signatures\n\n```go\n// Initialize new socketio in the callback this will\n// execute a callback that expects kws *Websocket Object\n// and optional config websocket.Config\nfunc New(callback func(kws *Websocket), config ...websocket.Config) func(fiber.Ctx) error\n```\n\n```go\n// Add listener callback for an event into the listeners list\nfunc On(event string, callback func(payload *EventPayload))\n```\n\n```go\n// Emit the message to a specific socket uuids list\n// Ignores all errors\nfunc EmitToList(uuids []string, message []byte)\n```\n\n```go\n// Emit to a specific socket connection\nfunc EmitTo(uuid string, message []byte) error\n```\n\n```go\n// Broadcast to all the active connections\n// except avoid broadcasting the message to itself\nfunc Broadcast(message []byte)\n```\n\n```go\n// Fire custom event on all connections\nfunc Fire(event string, data []byte) \n```\n\n## Example\n\n```go\npackage main\n\nimport (\n    \"encoding/json\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/gofiber/contrib/v3/socketio\"\n    \"github.com/gofiber/contrib/v3/websocket\"\n    \"github.com/gofiber/fiber/v3\"\n)\n\n// MessageObject Basic chat message object\ntype MessageObject struct {\n    Data  string `json:\"data\"`\n    From  string `json:\"from\"`\n    Event string `json:\"event\"`\n    To    string `json:\"to\"`\n}\n\nfunc main() {\n\n    // The key for the map is message.to\n    clients := make(map[string]string)\n\n    // Start a new Fiber application\n    app := fiber.New()\n\n    // Setup the middleware to retrieve the data sent in first GET request\n    app.Use(func(c fiber.Ctx) error {\n        // IsWebSocketUpgrade returns true if the client\n        // requested upgrade to the WebSocket protocol.\n        if websocket.IsWebSocketUpgrade(c) {\n            c.Locals(\"allowed\", true)\n            return c.Next()\n        }\n        return fiber.ErrUpgradeRequired\n    })\n\n    // Multiple event handling supported\n    socketio.On(socketio.EventConnect, func(ep *socketio.EventPayload) {\n        fmt.Printf(\"Connection event 1 - User: %s\", ep.Kws.GetStringAttribute(\"user_id\"))\n    })\n\n    // Custom event handling supported\n    socketio.On(\"CUSTOM_EVENT\", func(ep *socketio.EventPayload) {\n        fmt.Printf(\"Custom event - User: %s\", ep.Kws.GetStringAttribute(\"user_id\"))\n        // --->\n\n        // DO YOUR BUSINESS HERE\n\n        // --->\n    })\n\n    // On message event\n    socketio.On(socketio.EventMessage, func(ep *socketio.EventPayload) {\n\n        fmt.Printf(\"Message event - User: %s - Message: %s\", ep.Kws.GetStringAttribute(\"user_id\"), string(ep.Data))\n\n        message := MessageObject{}\n\n        // Unmarshal the json message\n        // {\n        //  \"from\": \"<user-id>\",\n        //  \"to\": \"<recipient-user-id>\",\n        //  \"event\": \"CUSTOM_EVENT\",\n        //  \"data\": \"hello\"\n        //}\n        err := json.Unmarshal(ep.Data, &message)\n        if err != nil {\n            fmt.Println(err)\n            return\n        }\n\n        // Fire custom event based on some\n        // business logic\n        if message.Event != \"\" {\n            ep.Kws.Fire(message.Event, []byte(message.Data))\n        }\n\n        // Emit the message directly to specified user\n        err = ep.Kws.EmitTo(clients[message.To], ep.Data, socketio.TextMessage)\n        if err != nil {\n            fmt.Println(err)\n        }\n    })\n\n    // On disconnect event\n    socketio.On(socketio.EventDisconnect, func(ep *socketio.EventPayload) {\n        // Remove the user from the local clients\n        delete(clients, ep.Kws.GetStringAttribute(\"user_id\"))\n        fmt.Printf(\"Disconnection event - User: %s\", ep.Kws.GetStringAttribute(\"user_id\"))\n    })\n\n    // On close event\n    // This event is called when the server disconnects the user actively with .Close() method\n    socketio.On(socketio.EventClose, func(ep *socketio.EventPayload) {\n        // Remove the user from the local clients\n        delete(clients, ep.Kws.GetStringAttribute(\"user_id\"))\n        fmt.Printf(\"Close event - User: %s\", ep.Kws.GetStringAttribute(\"user_id\"))\n    })\n\n    // On error event\n    socketio.On(socketio.EventError, func(ep *socketio.EventPayload) {\n        fmt.Printf(\"Error event - User: %s\", ep.Kws.GetStringAttribute(\"user_id\"))\n    })\n\n    app.Get(\"/ws/:id\", socketio.New(func(kws *socketio.Websocket) {\n\n        // Retrieve the user id from endpoint\n        userId := kws.Params(\"id\")\n\n        // Add the connection to the list of the connected clients\n        // The UUID is generated randomly and is the key that allow\n        // socketio to manage Emit/EmitTo/Broadcast\n        clients[userId] = kws.UUID\n\n        // Every websocket connection has an optional session key => value storage\n        kws.SetAttribute(\"user_id\", userId)\n\n        //Broadcast to all the connected users the newcomer\n        kws.Broadcast([]byte(fmt.Sprintf(\"New user connected: %s and UUID: %s\", userId, kws.UUID)), true, socketio.TextMessage)\n        //Write welcome message\n        kws.Emit([]byte(fmt.Sprintf(\"Hello user: %s with UUID: %s\", userId, kws.UUID)), socketio.TextMessage)\n    }))\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n\n```\n\n---\n\n## Supported events\n\n| Const           | Event        | Description                                                                                                                                                |\n|:----------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|\n| EventMessage    | `message`    | Fired when a Text/Binary message is received                                                                                                               |\n| EventPing       | `ping`       | [More details here](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets) |\n| EventPong       | `pong`       | Refer to ping description                                                                                                                                  |\n| EventDisconnect | `disconnect` | Fired on disconnection. The error provided in disconnection event as defined in RFC 6455, section 11.7.                                                    |\n| EventConnect    | `connect`    | Fired on first connection                                                                                                                                  |\n| EventClose      | `close`      | Fired when the connection is actively closed from the server. Different from client disconnection                                                          |\n| EventError      | `error`      | Fired when some error appears useful also for debugging websockets                                                                                         |\n\n## Event Payload object\n\n| Variable         | Type                | Description                                                                     |\n|:-----------------|:--------------------|:--------------------------------------------------------------------------------|\n| Kws              | `*Websocket`        | The connection object                                                           |\n| Name             | `string`            | The name of the event                                                           |\n| SocketUUID       | `string`            | Unique connection UUID                                                          |\n| SocketAttributes | `map[string]string` | Optional websocket attributes                                                   |\n| Error            | `error`             | (optional) Fired from disconnection or error events                             |\n| Data             | `[]byte`            | Data used on Message and on Error event, contains the payload for custom events |\n\n## Socket instance functions\n\n| Name         | Type     | Description                                                                       |\n|:-------------|:---------|:----------------------------------------------------------------------------------|\n| SetAttribute | `void`   | Set a specific attribute for the specific socket connection                       |\n| GetUUID      | `string` | Get socket connection UUID                                                        |\n| SetUUID      | `error`   | Set socket connection UUID                                                        |\n| GetAttribute | `string` | Get a specific attribute from the socket attributes                               |\n| EmitToList   | `void`   | Emit the message to a specific socket uuids list                                  |\n| EmitTo       | `error`  | Emit to a specific socket connection                                              |\n| Broadcast    | `void`   | Broadcast to all the active connections except broadcasting the message to itself |\n| Fire         | `void`   | Fire custom event                                                                 |\n| Emit         | `void`   | Emit/Write the message into the given connection                                  |\n| Close        | `void`   | Actively close the connection from the server                                     |\n\n**Note: the FastHTTP connection can be accessed directly from the instance**\n\n```go\nkws.Conn\n```\n"
  },
  {
    "path": "v3/socketio/go.mod",
    "content": "module github.com/gofiber/contrib/v3/socketio\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/fasthttp/websocket v1.5.12\n\tgithub.com/gofiber/contrib/v3/websocket v1.0.0\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/valyala/fasthttp v1.70.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect\n\tgithub.com/stretchr/objx v0.5.3 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nreplace github.com/gofiber/contrib/v3/websocket => ../websocket\n"
  },
  {
    "path": "v3/socketio/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=\ngithub.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=\ngithub.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=\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/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/socketio/socketio.go",
    "content": "package socketio\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/contrib/v3/websocket\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/google/uuid\"\n)\n\n// Source @url:https://github.com/gorilla/websocket/blob/master/conn.go#L61\n// The message types are defined in RFC 6455, section 11.8.\nconst (\n\t// TextMessage denotes a text data message. The text message payload is\n\t// interpreted as UTF-8 encoded text data.\n\tTextMessage = 1\n\t// BinaryMessage denotes a binary data message.\n\tBinaryMessage = 2\n\t// CloseMessage denotes a close control message. The optional message\n\t// payload contains a numeric code and text. Use the FormatCloseMessage\n\t// function to format a close message payload.\n\tCloseMessage = 8\n\t// PingMessage denotes a ping control message. The optional message payload\n\t// is UTF-8 encoded text.\n\tPingMessage = 9\n\t// PongMessage denotes a pong control message. The optional message payload\n\t// is UTF-8 encoded text.\n\tPongMessage = 10\n)\n\n// Supported event list\nconst (\n\t// EventMessage Fired when a Text/Binary message is received\n\tEventMessage = \"message\"\n\t// EventPing More details here:\n\t// @url https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets\n\tEventPing = \"ping\"\n\tEventPong = \"pong\"\n\t// EventDisconnect Fired on disconnection\n\t// The error provided in disconnection event\n\t// is defined in RFC 6455, section 11.7.\n\t// @url https://github.com/gofiber/websocket/blob/cd4720c435de415b864d975a9ca23a47eaf081ef/websocket.go#L192\n\tEventDisconnect = \"disconnect\"\n\t// EventConnect Fired on first connection\n\tEventConnect = \"connect\"\n\t// EventClose Fired when the connection is actively closed from the server\n\tEventClose = \"close\"\n\t// EventError Fired when some error appears useful also for debugging websockets\n\tEventError = \"error\"\n)\n\nvar (\n\t// ErrorInvalidConnection The addressed Conn connection is not available anymore\n\t// error data is the uuid of that connection\n\tErrorInvalidConnection = errors.New(\"message cannot be delivered invalid/gone connection\")\n\t// ErrorUUIDDuplication The UUID already exists in the pool\n\tErrorUUIDDuplication = errors.New(\"UUID already exists in the available connections pool\")\n)\n\nvar (\n\tPongTimeout = 1 * time.Second\n\t// RetrySendTimeout retry after 20 ms if there is an error\n\tRetrySendTimeout = 20 * time.Millisecond\n\t//MaxSendRetry define max retries if there are socket issues\n\tMaxSendRetry = 5\n\t// ReadTimeout Instead of reading in a for loop, try to avoid full CPU load taking some pause\n\tReadTimeout = 10 * time.Millisecond\n)\n\n// Raw form of websocket message\ntype message struct {\n\t// Message type\n\tmType int\n\t// Message data\n\tdata []byte\n\t// Message send retries when error\n\tretries int\n}\n\n// EventPayload Event Payload is the object that\n// stores all the information about the event and\n// the connection\ntype EventPayload struct {\n\t// The connection object\n\tKws *Websocket\n\t// The name of the event\n\tName string\n\t// Unique connection UUID\n\tSocketUUID string\n\t// Optional websocket attributes\n\tSocketAttributes map[string]any\n\t// Optional error when are fired events like\n\t// - Disconnect\n\t// - Error\n\tError error\n\t// Data is used on Message and on Error event\n\tData []byte\n}\n\ntype ws interface {\n\tIsAlive() bool\n\tGetUUID() string\n\tSetUUID(uuid string) error\n\tSetAttribute(key string, attribute interface{})\n\tGetAttribute(key string) interface{}\n\tGetIntAttribute(key string) int\n\tGetStringAttribute(key string) string\n\tEmitToList(uuids []string, message []byte, mType ...int)\n\tEmitTo(uuid string, message []byte, mType ...int) error\n\tBroadcast(message []byte, except bool, mType ...int)\n\tFire(event string, data []byte)\n\tEmit(message []byte, mType ...int)\n\tClose()\n\tpong(ctx context.Context)\n\twrite(messageType int, messageBytes []byte)\n\trun()\n\tread(ctx context.Context)\n\tdisconnected(err error)\n\tcreateUUID() string\n\trandomUUID() string\n\tfireEvent(event string, data []byte, error error)\n}\n\ntype Websocket struct {\n\tonce sync.Once\n\tmu   sync.RWMutex\n\t// The Fiber.Websocket connection\n\tConn *websocket.Conn\n\t// Define if the connection is alive or not\n\tisAlive bool\n\t// Queue of messages sent from the socket\n\tqueue chan message\n\t// Channel to signal when this websocket is closed\n\t// so go routines will stop gracefully\n\tdone chan struct{}\n\t// Attributes map collection for the connection\n\tattributes map[string]interface{}\n\t// Unique id of the connection\n\tUUID string\n\t// Wrap Fiber Locals function\n\tLocals func(key string) interface{}\n\t// Wrap Fiber Params function\n\tParams func(key string, defaultValue ...string) string\n\t// Wrap Fiber Query function\n\tQuery func(key string, defaultValue ...string) string\n\t// Wrap Fiber Cookies function\n\tCookies func(key string, defaultValue ...string) string\n}\n\ntype safePool struct {\n\tsync.RWMutex\n\t// List of the connections alive\n\tconn map[string]ws\n}\n\n// Pool with the active connections\nvar pool = safePool{\n\tconn: make(map[string]ws),\n}\n\nfunc (p *safePool) set(ws ws) {\n\tp.Lock()\n\tp.conn[ws.GetUUID()] = ws\n\tp.Unlock()\n}\n\nfunc (p *safePool) all() map[string]ws {\n\tp.RLock()\n\tret := make(map[string]ws, 0)\n\tfor wsUUID, kws := range p.conn {\n\t\tret[wsUUID] = kws\n\t}\n\tp.RUnlock()\n\treturn ret\n}\n\nfunc (p *safePool) get(key string) (ws, error) {\n\tp.RLock()\n\tret, ok := p.conn[key]\n\tp.RUnlock()\n\tif !ok {\n\t\treturn nil, ErrorInvalidConnection\n\t}\n\treturn ret, nil\n}\n\nfunc (p *safePool) contains(key string) bool {\n\tp.RLock()\n\t_, ok := p.conn[key]\n\tp.RUnlock()\n\treturn ok\n}\n\nfunc (p *safePool) delete(key string) {\n\tp.Lock()\n\tdelete(p.conn, key)\n\tp.Unlock()\n}\n\n//nolint:all\nfunc (p *safePool) reset() {\n\tp.Lock()\n\tp.conn = make(map[string]ws)\n\tp.Unlock()\n}\n\ntype safeListeners struct {\n\tsync.RWMutex\n\tlist map[string][]eventCallback\n}\n\nfunc (l *safeListeners) set(event string, callback eventCallback) {\n\tl.Lock()\n\tlisteners.list[event] = append(listeners.list[event], callback)\n\tl.Unlock()\n}\n\nfunc (l *safeListeners) get(event string) []eventCallback {\n\tl.RLock()\n\tdefer l.RUnlock()\n\tif _, ok := l.list[event]; !ok {\n\t\treturn make([]eventCallback, 0)\n\t}\n\n\tret := make([]eventCallback, 0)\n\tret = append(ret, l.list[event]...)\n\treturn ret\n}\n\n// List of the listeners for the events\nvar listeners = safeListeners{\n\tlist: make(map[string][]eventCallback),\n}\n\nfunc New(callback func(kws *Websocket), config ...websocket.Config) func(fiber.Ctx) error {\n\treturn websocket.New(func(c *websocket.Conn) {\n\t\tkws := &Websocket{\n\t\t\tConn: c,\n\t\t\tLocals: func(key string) interface{} {\n\t\t\t\treturn c.Locals(key)\n\t\t\t},\n\t\t\tParams: func(key string, defaultValue ...string) string {\n\t\t\t\treturn c.Params(key, defaultValue...)\n\t\t\t},\n\t\t\tQuery: func(key string, defaultValue ...string) string {\n\t\t\t\treturn c.Query(key, defaultValue...)\n\t\t\t},\n\t\t\tCookies: func(key string, defaultValue ...string) string {\n\t\t\t\treturn c.Cookies(key, defaultValue...)\n\t\t\t},\n\t\t\tqueue:      make(chan message, 100),\n\t\t\tdone:       make(chan struct{}, 1),\n\t\t\tattributes: make(map[string]interface{}),\n\t\t\tisAlive:    true,\n\t\t}\n\n\t\t// Generate uuid\n\t\tkws.UUID = kws.createUUID()\n\n\t\t// register the connection into the pool\n\t\tpool.set(kws)\n\n\t\t// execute the callback of the socket initialization\n\t\tcallback(kws)\n\n\t\tkws.fireEvent(EventConnect, nil, nil)\n\n\t\t// Run the loop for the given connection\n\t\tkws.run()\n\t}, config...)\n}\n\nfunc (kws *Websocket) GetUUID() string {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\treturn kws.UUID\n}\n\nfunc (kws *Websocket) SetUUID(uuid string) error {\n\tpool.Lock()\n\tdefer pool.Unlock()\n\tkws.mu.Lock()\n\tdefer kws.mu.Unlock()\n\n\tprevUUID := kws.UUID\n\tif prevUUID == uuid {\n\t\treturn nil\n\t}\n\tkws.UUID = uuid\n\n\tif existing, ok := pool.conn[uuid]; ok && existing != kws {\n\t\tkws.UUID = prevUUID\n\t\treturn ErrorUUIDDuplication\n\t}\n\n\tif prevUUID != \"\" {\n\t\tdelete(pool.conn, prevUUID)\n\t}\n\tpool.conn[uuid] = kws\n\treturn nil\n}\n\n// SetAttribute Set a specific attribute for the specific socket connection\nfunc (kws *Websocket) SetAttribute(key string, attribute interface{}) {\n\tkws.mu.Lock()\n\tdefer kws.mu.Unlock()\n\tkws.attributes[key] = attribute\n}\n\n// GetAttribute Get a specific attribute from the socket attributes\nfunc (kws *Websocket) GetAttribute(key string) interface{} {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\tvalue, ok := kws.attributes[key]\n\tif ok {\n\t\treturn value\n\t}\n\treturn nil\n}\n\n// GetIntAttribute Convenience method to retrieve an attribute as an int.\n// Will panic if attribute is not an int.\nfunc (kws *Websocket) GetIntAttribute(key string) int {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\tvalue, ok := kws.attributes[key]\n\tif ok {\n\t\treturn value.(int)\n\t}\n\treturn 0\n}\n\n// GetStringAttribute Convenience method to retrieve an attribute as a string.\nfunc (kws *Websocket) GetStringAttribute(key string) string {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\tvalue, ok := kws.attributes[key]\n\tif ok {\n\t\treturn value.(string)\n\t}\n\treturn \"\"\n}\n\n// EmitToList Emit the message to a specific socket uuids list\nfunc (kws *Websocket) EmitToList(uuids []string, message []byte, mType ...int) {\n\tfor _, wsUUID := range uuids {\n\t\terr := kws.EmitTo(wsUUID, message, mType...)\n\t\tif err != nil {\n\t\t\tkws.fireEvent(EventError, message, err)\n\t\t}\n\t}\n}\n\n// EmitToList Emit the message to a specific socket uuids list\n// Ignores all errors\nfunc EmitToList(uuids []string, message []byte, mType ...int) {\n\tfor _, wsUUID := range uuids {\n\t\t_ = EmitTo(wsUUID, message, mType...)\n\t}\n}\n\n// EmitTo Emit to a specific socket connection\nfunc (kws *Websocket) EmitTo(uuid string, message []byte, mType ...int) error {\n\n\tconn, err := pool.get(uuid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !pool.contains(uuid) || !conn.IsAlive() {\n\t\tkws.fireEvent(EventError, []byte(uuid), ErrorInvalidConnection)\n\t\treturn ErrorInvalidConnection\n\t}\n\n\tconn.Emit(message, mType...)\n\treturn nil\n}\n\n// EmitTo Emit to a specific socket connection\nfunc EmitTo(uuid string, message []byte, mType ...int) error {\n\tconn, err := pool.get(uuid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !pool.contains(uuid) || !conn.IsAlive() {\n\t\treturn ErrorInvalidConnection\n\t}\n\n\tconn.Emit(message, mType...)\n\treturn nil\n}\n\n// Broadcast to all the active connections\n// except avoid broadcasting the message to itself\nfunc (kws *Websocket) Broadcast(message []byte, except bool, mType ...int) {\n\tfor wsUUID := range pool.all() {\n\t\tif except && kws.UUID == wsUUID {\n\t\t\tcontinue\n\t\t}\n\t\terr := kws.EmitTo(wsUUID, message, mType...)\n\t\tif err != nil {\n\t\t\tkws.fireEvent(EventError, message, err)\n\t\t}\n\t}\n}\n\n// Broadcast to all the active connections\nfunc Broadcast(message []byte, mType ...int) {\n\tfor _, kws := range pool.all() {\n\t\tkws.Emit(message, mType...)\n\t}\n}\n\n// Fire custom event\nfunc (kws *Websocket) Fire(event string, data []byte) {\n\tkws.fireEvent(event, data, nil)\n}\n\n// Fire custom event on all connections\nfunc Fire(event string, data []byte) {\n\tfireGlobalEvent(event, data, nil)\n}\n\n// Emit /Write the message into the given connection\nfunc (kws *Websocket) Emit(message []byte, mType ...int) {\n\tt := TextMessage\n\tif len(mType) > 0 {\n\t\tt = mType[0]\n\t}\n\tkws.write(t, message)\n}\n\n// Close Actively close the connection from the server\nfunc (kws *Websocket) Close() {\n\tkws.write(CloseMessage, []byte(\"Connection closed\"))\n\tkws.fireEvent(EventClose, nil, nil)\n}\n\nfunc (kws *Websocket) IsAlive() bool {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\treturn kws.isAlive\n}\n\nfunc (kws *Websocket) hasConn() bool {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\treturn kws.Conn.Conn != nil\n}\n\nfunc (kws *Websocket) setAlive(alive bool) {\n\tkws.mu.Lock()\n\tdefer kws.mu.Unlock()\n\tkws.isAlive = alive\n}\n\n//nolint:all\nfunc (kws *Websocket) queueLength() int {\n\tkws.mu.RLock()\n\tdefer kws.mu.RUnlock()\n\treturn len(kws.queue)\n}\n\n// pong writes a control message to the client\nfunc (kws *Websocket) pong(ctx context.Context) {\n\ttimeoutTicker := time.NewTicker(PongTimeout)\n\tdefer timeoutTicker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-timeoutTicker.C:\n\t\t\tkws.write(PongMessage, []byte{})\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Add in message queue\nfunc (kws *Websocket) write(messageType int, messageBytes []byte) {\n\tkws.queue <- message{\n\t\tmType:   messageType,\n\t\tdata:    messageBytes,\n\t\tretries: 0,\n\t}\n}\n\n// Send out message queue\nfunc (kws *Websocket) send(ctx context.Context) {\n\tfor {\n\t\tselect {\n\t\tcase message := <-kws.queue:\n\t\t\tif !kws.hasConn() {\n\t\t\t\tif message.retries <= MaxSendRetry {\n\t\t\t\t\t// retry without blocking the sending thread\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\ttime.Sleep(RetrySendTimeout)\n\t\t\t\t\t\tmessage.retries = message.retries + 1\n\t\t\t\t\t\tkws.queue <- message\n\t\t\t\t\t}()\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tkws.mu.RLock()\n\t\t\terr := kws.Conn.WriteMessage(message.mType, message.data)\n\t\t\tkws.mu.RUnlock()\n\n\t\t\tif err != nil {\n\t\t\t\tkws.disconnected(err)\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Start Pong/Read/Write functions\n//\n// Needs to be blocking, otherwise the connection would close.\nfunc (kws *Websocket) run() {\n\tctx, cancelFunc := context.WithCancel(context.Background())\n\n\tgo kws.pong(ctx)\n\tgo kws.read(ctx)\n\tgo kws.send(ctx)\n\n\t<-kws.done // block until one event is sent to the done channel\n\n\tcancelFunc()\n}\n\n// Listen for incoming messages\n// and filter by message type\nfunc (kws *Websocket) read(ctx context.Context) {\n\ttimeoutTicker := time.NewTicker(ReadTimeout)\n\tdefer timeoutTicker.Stop()\n\tfor {\n\t\tselect {\n\t\tcase <-timeoutTicker.C:\n\t\t\tif !kws.hasConn() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tkws.mu.RLock()\n\t\t\tmType, msg, err := kws.Conn.ReadMessage()\n\t\t\tkws.mu.RUnlock()\n\n\t\t\tif mType == PingMessage {\n\t\t\t\tkws.fireEvent(EventPing, nil, nil)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif mType == PongMessage {\n\t\t\t\tkws.fireEvent(EventPong, nil, nil)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif mType == CloseMessage {\n\t\t\t\tkws.disconnected(nil)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tkws.disconnected(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// We have a message and we fire the message event\n\t\t\tkws.fireEvent(EventMessage, msg, nil)\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// When the connection closes, disconnected method\nfunc (kws *Websocket) disconnected(err error) {\n\tkws.fireEvent(EventDisconnect, nil, err)\n\n\t// may be called multiple times from different go routines\n\tif kws.IsAlive() {\n\t\tkws.once.Do(func() {\n\t\t\tkws.setAlive(false)\n\t\t\tclose(kws.done)\n\t\t})\n\t}\n\n\t// Fire error event if the connection is\n\t// disconnected by an error\n\tif err != nil {\n\t\tkws.fireEvent(EventError, nil, err)\n\t}\n\n\t// Remove the socket from the pool\n\tpool.delete(kws.UUID)\n}\n\n// Create random UUID for each connection\nfunc (kws *Websocket) createUUID() string {\n\treturn kws.randomUUID()\n}\n\n// Generate random UUID.\nfunc (kws *Websocket) randomUUID() string {\n\treturn uuid.New().String()\n}\n\n// Fires event on all connections.\nfunc fireGlobalEvent(event string, data []byte, error error) {\n\tfor _, kws := range pool.all() {\n\t\tkws.fireEvent(event, data, error)\n\t}\n}\n\n// Checks if there is at least a listener for a given event\n// and loop over the callbacks registered\nfunc (kws *Websocket) fireEvent(event string, data []byte, error error) {\n\tcallbacks := listeners.get(event)\n\n\tfor _, callback := range callbacks {\n\t\tcallback(&EventPayload{\n\t\t\tKws:              kws,\n\t\t\tName:             event,\n\t\t\tSocketUUID:       kws.UUID,\n\t\t\tSocketAttributes: kws.attributes,\n\t\t\tData:             data,\n\t\t\tError:            error,\n\t\t})\n\t}\n}\n\ntype eventCallback func(payload *EventPayload)\n\n// On Add listener callback for an event into the listeners list\nfunc On(event string, callback eventCallback) {\n\tlisteners.set(event, callback)\n}\n"
  },
  {
    "path": "v3/socketio/socketio_test.go",
    "content": "package socketio\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fasthttp/websocket\"\n\tfws \"github.com/gofiber/contrib/v3/websocket\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/valyala/fasthttp/fasthttputil\"\n)\n\nconst numTestConn = 10\nconst numParallelTestConn = 5_000\n\ntype HandlerMock struct {\n\tmock.Mock\n\twg sync.WaitGroup\n}\n\ntype WebsocketMock struct {\n\tmock.Mock\n\tmu         sync.RWMutex\n\twg         sync.WaitGroup\n\tConn       *websocket.Conn\n\tisAlive    bool\n\tqueue      map[string]message\n\tattributes map[string]string\n\tUUID       string\n\tLocals     func(key string) interface{}\n\tParams     func(key string, defaultValue ...string) string\n\tQuery      func(key string, defaultValue ...string) string\n\tCookies    func(key string, defaultValue ...string) string\n}\n\nfunc (s *WebsocketMock) SetUUID(uuid string) error {\n\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif pool.contains(uuid) {\n\t\tpanic(ErrorUUIDDuplication)\n\t}\n\ts.UUID = uuid\n\treturn nil\n}\n\nfunc (s *WebsocketMock) GetIntAttribute(key string) int {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\tvalue, ok := s.attributes[key]\n\tif ok {\n\t\tif intValue, err := strconv.Atoi(value); err == nil {\n\t\t\treturn intValue\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (s *WebsocketMock) GetStringAttribute(key string) string {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\tvalue, ok := s.attributes[key]\n\tif ok {\n\t\treturn value\n\t}\n\treturn \"\"\n}\n\nfunc (h *HandlerMock) OnCustomEvent(payload *EventPayload) {\n\th.Called(payload)\n\th.wg.Done()\n}\n\nfunc (s *WebsocketMock) Emit(message []byte, _ ...int) {\n\ts.Called(message)\n\ts.wg.Done()\n}\n\nfunc (s *WebsocketMock) IsAlive() bool {\n\targs := s.Called()\n\treturn args.Bool(0)\n}\n\nfunc (s *WebsocketMock) GetUUID() string {\n\treturn s.UUID\n}\n\nfunc TestParallelConnections(t *testing.T) {\n\tpool.reset()\n\n\t// create test server\n\tcfg := fiber.Config{}\n\tapp := fiber.New(cfg)\n\tln := fasthttputil.NewInmemoryListener()\n\twg := sync.WaitGroup{}\n\n\tdefer func() {\n\t\t_ = app.Shutdown()\n\t\t_ = ln.Close()\n\t}()\n\n\t// attach upgrade middleware\n\tapp.Use(upgradeMiddleware)\n\n\t// send back response on correct message\n\tOn(EventMessage, func(payload *EventPayload) {\n\t\tif string(payload.Data) == \"test\" {\n\t\t\tpayload.Kws.Emit([]byte(\"response\"))\n\t\t}\n\t})\n\n\t// create websocket endpoint\n\tapp.Get(\"/\", New(func(kws *Websocket) {\n\t}))\n\n\t// start server\n\tgo func() {\n\t\t_ = app.Listener(ln)\n\t}()\n\n\twsURL := \"ws://\" + ln.Addr().String()\n\n\t// create concurrent connections\n\tfor i := 0; i < numParallelTestConn; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdialer := &websocket.Dialer{\n\t\t\t\tNetDial: func(network, addr string) (net.Conn, error) {\n\t\t\t\t\treturn ln.Dial()\n\t\t\t\t},\n\t\t\t\tHandshakeTimeout: 45 * time.Second,\n\t\t\t}\n\t\t\tdial, _, err := dialer.Dial(wsURL, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err := dial.WriteMessage(websocket.TextMessage, []byte(\"test\")); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttp, m, err := dial.ReadMessage()\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.Equal(t, TextMessage, tp)\n\t\t\trequire.Equal(t, \"response\", string(m))\n\t\t\twg.Done()\n\n\t\t\tif err := dial.Close(); err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\t}\n\twg.Wait()\n}\n\nfunc TestGlobalFire(t *testing.T) {\n\tpool.reset()\n\n\t// simulate connections\n\tfor i := 0; i < numTestConn; i++ {\n\t\tkws := createWS()\n\t\tpool.set(kws)\n\t}\n\n\th := new(HandlerMock)\n\t// setup expectations\n\th.On(\"OnCustomEvent\", mock.Anything).Return(nil)\n\n\t// Moved before registration of the event\n\t// if after can cause: panic: sync: negative WaitGroup counter\n\th.wg.Add(numTestConn)\n\n\t// register custom event handler\n\tOn(\"customevent\", h.OnCustomEvent)\n\n\t// fire global custom event on all connections\n\tFire(\"customevent\", []byte(\"test\"))\n\n\th.wg.Wait()\n\n\th.AssertNumberOfCalls(t, \"OnCustomEvent\", numTestConn)\n}\n\nfunc TestGlobalBroadcast(t *testing.T) {\n\tpool.reset()\n\n\tfor i := 0; i < numParallelTestConn; i++ {\n\t\tmws := new(WebsocketMock)\n\t\tmws.SetUUID(mws.createUUID())\n\t\tpool.set(mws)\n\n\t\t// setup expectations\n\t\tmws.On(\"Emit\", mock.Anything).Return(nil)\n\n\t\tmws.wg.Add(1)\n\t}\n\n\t// send global broadcast to all connections\n\tBroadcast([]byte(\"test\"), TextMessage)\n\n\tfor _, mws := range pool.all() {\n\t\tmws.(*WebsocketMock).wg.Wait()\n\t\tmws.(*WebsocketMock).AssertNumberOfCalls(t, \"Emit\", 1)\n\t}\n\n}\n\nfunc TestGlobalEmitTo(t *testing.T) {\n\tpool.reset()\n\n\taliveUUID := \"80a80sdf809dsf\"\n\tclosedUUID := \"las3dfj09808\"\n\n\talive := new(WebsocketMock)\n\talive.UUID = aliveUUID\n\tpool.set(alive)\n\n\tclosed := new(WebsocketMock)\n\tclosed.UUID = closedUUID\n\tpool.set(closed)\n\n\t// setup expectations\n\talive.On(\"Emit\", mock.Anything).Return(nil)\n\talive.On(\"IsAlive\").Return(true)\n\tclosed.On(\"IsAlive\").Return(false)\n\n\tvar err error\n\terr = EmitTo(\"non-existent\", []byte(\"error\"))\n\trequire.Equal(t, ErrorInvalidConnection, err)\n\n\terr = EmitTo(closedUUID, []byte(\"error\"))\n\trequire.Equal(t, ErrorInvalidConnection, err)\n\n\talive.wg.Add(1)\n\n\t// send global broadcast to all connections\n\terr = EmitTo(aliveUUID, []byte(\"test\"))\n\trequire.Nil(t, err)\n\n\talive.wg.Wait()\n\n\talive.AssertNumberOfCalls(t, \"Emit\", 1)\n}\n\nfunc TestGlobalEmitToList(t *testing.T) {\n\tpool.reset()\n\n\tuuids := []string{\n\t\t\"80a80sdf809dsf\",\n\t\t\"las3dfj09808\",\n\t}\n\n\tfor _, id := range uuids {\n\t\tkws := new(WebsocketMock)\n\t\tkws.SetUUID(id)\n\t\tkws.On(\"Emit\", mock.Anything).Return(nil)\n\t\tkws.On(\"IsAlive\").Return(true)\n\t\tkws.wg.Add(1)\n\t\tpool.set(kws)\n\t}\n\n\t// send global broadcast to all connections\n\tEmitToList(uuids, []byte(\"test\"), TextMessage)\n\n\tfor _, kws := range pool.all() {\n\t\tkws.(*WebsocketMock).wg.Wait()\n\t\tkws.(*WebsocketMock).AssertNumberOfCalls(t, \"Emit\", 1)\n\t}\n}\n\nfunc TestWebsocket_GetIntAttribute(t *testing.T) {\n\tkws := &Websocket{\n\t\tattributes: make(map[string]interface{}),\n\t}\n\n\t// get unset attribute\n\t// Will return null without panicking\n\n\t// get non-int attribute\n\t// Will return 0 without panicking\n\tkws.SetAttribute(\"notInt\", \"\")\n\n\t// get int attribute\n\tkws.SetAttribute(\"int\", 3)\n\tv := kws.GetIntAttribute(\"int\")\n\trequire.Equal(t, 3, v)\n}\n\nfunc TestWebsocket_GetStringAttribute(t *testing.T) {\n\tkws := &Websocket{\n\t\tattributes: make(map[string]interface{}),\n\t}\n\n\t// get unset attribute\n\n\t// get non-string attribute\n\tkws.SetAttribute(\"notString\", 3)\n\n\t// get string attribute\n\tkws.SetAttribute(\"str\", \"3\")\n\tv := kws.GetStringAttribute(\"str\")\n\trequire.Equal(t, \"3\", v)\n}\n\nfunc TestWebsocket_SetUUIDUpdatesPool(t *testing.T) {\n\tpool.reset()\n\n\tkws := createWS()\n\tpool.set(kws)\n\n\toldUUID := kws.GetUUID()\n\tnewUUID := \"new-uuid\"\n\n\terr := kws.SetUUID(newUUID)\n\trequire.NoError(t, err)\n\trequire.Equal(t, newUUID, kws.GetUUID())\n\n\t_, err = pool.get(oldUUID)\n\trequire.ErrorIs(t, err, ErrorInvalidConnection)\n\n\tpoolEntry, err := pool.get(newUUID)\n\trequire.NoError(t, err)\n\trequire.Equal(t, kws, poolEntry)\n\n\tother := createWS()\n\tother.UUID = \"other-uuid\"\n\tpool.set(other)\n\n\terr = kws.SetUUID(other.UUID)\n\trequire.ErrorIs(t, err, ErrorUUIDDuplication)\n\trequire.Equal(t, newUUID, kws.GetUUID())\n\n\tpoolEntry, err = pool.get(newUUID)\n\trequire.NoError(t, err)\n\trequire.Equal(t, kws, poolEntry)\n}\n\nfunc assertPanic(t *testing.T, f func()) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\tf()\n}\n\nfunc createWS() *Websocket {\n\tkws := &Websocket{\n\t\tConn: nil,\n\t\tLocals: func(key string) interface{} {\n\t\t\treturn \"\"\n\t\t},\n\t\tParams: func(key string, defaultValue ...string) string {\n\t\t\treturn \"\"\n\t\t},\n\t\tQuery: func(key string, defaultValue ...string) string {\n\t\t\treturn \"\"\n\t\t},\n\t\tCookies: func(key string, defaultValue ...string) string {\n\t\t\treturn \"\"\n\t\t},\n\t\tqueue:      make(chan message),\n\t\tattributes: make(map[string]interface{}),\n\t\tisAlive:    true,\n\t}\n\n\tkws.UUID = kws.createUUID()\n\n\treturn kws\n}\n\nfunc upgradeMiddleware(c fiber.Ctx) error {\n\t// IsWebSocketUpgrade returns true if the client\n\t// requested upgrade to the WebSocket protocol.\n\tif fws.IsWebSocketUpgrade(c) {\n\t\tfiber.StoreInContext(c, \"allowed\", true)\n\t\treturn c.Next()\n\t}\n\treturn fiber.ErrUpgradeRequired\n}\n\n//\n// needed but not used\n//\n\nfunc (s *WebsocketMock) SetAttribute(_ string, _ interface{}) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) GetAttribute(_ string) interface{} {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) EmitToList(_ []string, _ []byte, _ ...int) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) EmitTo(_ string, _ []byte, _ ...int) error {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) Broadcast(_ []byte, _ bool, _ ...int) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) Fire(_ string, _ []byte) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) Close() {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) pong(_ context.Context) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) write(_ int, _ []byte) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) run() {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) read(_ context.Context) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) disconnected(_ error) {\n\tpanic(\"implement me\")\n}\n\nfunc (s *WebsocketMock) createUUID() string {\n\treturn s.randomUUID()\n}\n\nfunc (s *WebsocketMock) randomUUID() string {\n\treturn uuid.New().String()\n}\n\nfunc (s *WebsocketMock) fireEvent(_ string, _ []byte, _ error) {\n\tpanic(\"implement me\")\n}\n"
  },
  {
    "path": "v3/swaggerui/README.md",
    "content": "---\nid: swaggerui\n---\n\n# Swagger UI\n\n> ⚠️ This module was renamed from `gofiber/contrib/swagger` to `swaggerui` to clearly distinguish it from the ported `swaggo` middleware. Update your imports accordingly.\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*swaggerui*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20swaggerui/badge.svg)\n\nSwagger UI middleware for [Fiber](https://github.com/gofiber/fiber). This handler serves pre-generated Swagger/OpenAPI specs via the swagger-ui package.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n### Table of Contents\n- [Signatures](#signatures)\n- [Installation](#installation)\n- [Examples](#examples)\n- [Config](#config)\n- [Default Config](#default-config)\n\n### Signatures\n```go\nfunc New(config ...swaggerui.Config) fiber.Handler\n```\n\n### Installation\nSwagger is tested on the latests [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet:\n```bash\ngo mod init github.com/<user>/<repo>\n```\nAnd then install the Swagger UI middleware:\n```bash\ngo get github.com/gofiber/contrib/v3/swaggerui\n```\n\n### Examples\nImport the middleware package\n```go\nimport (\n  \"github.com/gofiber/fiber/v3\"\n  \"github.com/gofiber/contrib/v3/swaggerui\"\n)\n```\n\nUsing the default config:\n```go\napp.Use(swaggerui.New())\n```\n\nUsing a custom config:\n```go\ncfg := swaggerui.Config{\n    BasePath: \"/\",\n    FilePath: \"./docs/swagger.json\",\n    Path:     \"swagger\",\n    Title:    \"Swagger API Docs\",\n}\n\napp.Use(swaggerui.New(cfg))\n```\n\nUse program data for Swagger content:\n```go\ncfg := swaggerui.Config{\n    BasePath:    \"/\",\n    FilePath:    \"./docs/swagger.json\",\n    FileContent: mySwaggerByteSlice,\n    Path:        \"swagger\",\n    Title:       \"Swagger API Docs\",\n}\n\napp.Use(swaggerui.New(cfg))\n```\n\nUsing multiple instances of Swagger:\n```go\n// Create Swagger middleware for v1\n//\n// Swagger will be available at: /api/v1/docs\napp.Use(swaggerui.New(swaggerui.Config{\n    BasePath: \"/api/v1/\",\n    FilePath: \"./docs/v1/swagger.json\",\n    Path:     \"docs\",\n}))\n\n// Create Swagger middleware for a second API version\n//\n// Swagger will be available at: /api/v2/docs\napp.Use(swaggerui.New(swaggerui.Config{\n    BasePath: \"/api/v2/\",\n    FilePath: \"./docs/v2/swagger.json\",\n    Path:     \"docs\",\n}))\n```\n\n### Config\n```go\ntype Config struct {\n    // Next defines a function to skip this middleware when returned true.\n    //\n    // Optional. Default: nil\n    Next func(c fiber.Ctx) bool\n\n    // BasePath for the UI path\n    //\n    // Optional. Default: /\n    BasePath string\n\n    // FilePath for the swagger.json or swagger.yaml file\n    //\n    // Optional. Default: ./swagger.json\n    FilePath string\n\n    // FileContent for the content of the swagger.json or swagger.yaml file.\n    // If provided, FilePath will not be read.\n    //\n    // Optional. Default: nil\n    FileContent []byte\n\n    // Path combines with BasePath for the full UI path\n    //\n    // Optional. Default: docs\n    Path string\n\n    // Title for the documentation site\n    //\n    // Optional. Default: Fiber API documentation\n    Title string\n\n    // CacheAge defines the max-age for the Cache-Control header in seconds.\n    //\n    // Optional. Default: 3600 (1 hour)\n    CacheAge int\n}\n```\n\n### Default Config\n```go\nvar ConfigDefault = Config{\n    Next:     nil,\n    BasePath: \"/\",\n    FilePath: \"./swagger.json\",\n    Path:     \"docs\",\n    Title:    \"Fiber API documentation\",\n    CacheAge: 3600, // Default to 1 hour\n}\n```\n"
  },
  {
    "path": "v3/swaggerui/go.mod",
    "content": "module github.com/gofiber/contrib/v3/swaggerui\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/go-openapi/runtime v0.29.4\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgopkg.in/yaml.v2 v2.4.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/go-openapi/analysis v0.25.0 // indirect\n\tgithub.com/go-openapi/errors v0.22.7 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.23.1 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.5 // indirect\n\tgithub.com/go-openapi/loads v0.23.3 // indirect\n\tgithub.com/go-openapi/spec v0.22.4 // indirect\n\tgithub.com/go-openapi/strfmt v0.26.1 // indirect\n\tgithub.com/go-openapi/swag/conv v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/fileutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/loading v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/mangling v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.26.0 // indirect\n\tgithub.com/go-openapi/validate v0.25.2 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/oklog/ulid/v2 v2.1.1 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/swaggerui/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs=\ngithub.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE=\ngithub.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=\ngithub.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=\ngithub.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=\ngithub.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=\ngithub.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=\ngithub.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=\ngithub.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=\ngithub.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=\ngithub.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo=\ngithub.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18=\ngithub.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=\ngithub.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=\ngithub.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=\ngithub.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=\ngithub.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=\ngithub.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=\ngithub.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU=\ngithub.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc=\ngithub.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=\ngithub.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=\ngithub.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=\ngithub.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=\ngithub.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=\ngithub.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=\ngithub.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ=\ngithub.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0=\ngithub.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=\ngithub.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=\ngithub.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=\ngithub.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=\ngithub.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=\ngithub.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=\ngithub.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=\ngithub.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=\ngithub.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=\ngithub.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=\ngithub.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=\ngithub.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\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": "v3/swaggerui/swagger.go",
    "content": "package swaggerui\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/go-openapi/runtime/middleware\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/adaptor\"\n\t\"gopkg.in/yaml.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// BasePath for the UI path\n\t//\n\t// Optional. Default: /\n\tBasePath string\n\n\t// FilePath for the swagger.json or swagger.yaml file\n\t//\n\t// Optional. Default: ./swagger.json\n\tFilePath string\n\n\t// FileContent for the content of the swagger.json or swagger.yaml file.\n\t// If provided, FilePath will not be read.\n\t//\n\t// Optional. Default: nil\n\tFileContent []byte\n\n\t// Path combines with BasePath for the full UI path\n\t//\n\t// Optional. Default: docs\n\tPath string\n\n\t// Title for the documentation site\n\t//\n\t// Optional. Default: Fiber API documentation\n\tTitle string\n\n\t// CacheAge defines the max-age for the Cache-Control header in seconds.\n\t//\n\t// Optional. Default: 3600 (1 hour)\n\tCacheAge int\n\n\t// The three components needed to embed swagger-ui\n\n\t// SwaggerURL points to the js that generates the SwaggerUI site.\n\t//\n\t// Defaults to: https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js\n\tSwaggerURL string\n\n\tSwaggerPresetURL string\n\tSwaggerStylesURL string\n\n\tFavicon32 string\n\tFavicon16 string\n}\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:     nil,\n\tBasePath: \"/\",\n\tFilePath: \"./swagger.json\",\n\tPath:     \"docs\",\n\tTitle:    \"Fiber API documentation\",\n\tCacheAge: 3600, // Default to 1 hour\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.BasePath) == 0 {\n\t\t\tcfg.BasePath = ConfigDefault.BasePath\n\t\t}\n\t\tif len(cfg.FilePath) == 0 {\n\t\t\tcfg.FilePath = ConfigDefault.FilePath\n\t\t}\n\t\tif len(cfg.Path) == 0 {\n\t\t\tcfg.Path = ConfigDefault.Path\n\t\t}\n\t\tif len(cfg.Title) == 0 {\n\t\t\tcfg.Title = ConfigDefault.Title\n\t\t}\n\t\tif cfg.CacheAge == 0 {\n\t\t\tcfg.CacheAge = ConfigDefault.CacheAge\n\t\t}\n\t}\n\n\trawSpec := cfg.FileContent\n\tif len(rawSpec) == 0 {\n\t\t// Verify Swagger file exists\n\t\t_, err := os.Stat(cfg.FilePath)\n\t\tif os.IsNotExist(err) {\n\t\t\tpanic(fmt.Errorf(\"%s file does not exist\", cfg.FilePath))\n\t\t}\n\n\t\t// Read Swagger Spec into memory\n\t\trawSpec, err = os.ReadFile(cfg.FilePath)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Failed to read provided Swagger file (%s): %v\", cfg.FilePath, err.Error())\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// Validate we have valid JSON or YAML\n\tvar jsonData map[string]interface{}\n\terrJSON := json.Unmarshal(rawSpec, &jsonData)\n\tvar yamlData map[string]interface{}\n\terrYAML := yaml.Unmarshal(rawSpec, &yamlData)\n\n\tif errJSON != nil && errYAML != nil {\n\t\tlog.Fatalf(\"Failed to parse the Swagger spec as JSON or YAML: JSON error: %s, YAML error: %s\", errJSON, errYAML)\n\t\tif len(cfg.FileContent) != 0 {\n\t\t\tpanic(fmt.Errorf(\"Invalid Swagger spec: %s\", string(rawSpec)))\n\t\t}\n\t\tpanic(fmt.Errorf(\"Invalid Swagger spec file: %s\", cfg.FilePath))\n\t}\n\n\t// Generate URL path's for the middleware\n\tspecURL := path.Join(cfg.BasePath, cfg.FilePath)\n\tswaggerUIPath := path.Join(cfg.BasePath, cfg.Path)\n\n\t// Serve the Swagger spec from memory\n\tswaggerSpecHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif strings.HasSuffix(r.URL.Path, \".yaml\") || strings.HasSuffix(r.URL.Path, \".yml\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/yaml\")\n\t\t\tw.Header().Set(\"Cache-Control\", fmt.Sprintf(\"public, max-age=%d\", cfg.CacheAge))\n\t\t\t_, err := w.Write(rawSpec)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, \"Error processing YAML Swagger Spec\", http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else if strings.HasSuffix(r.URL.Path, \".json\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.Header().Set(\"Cache-Control\", fmt.Sprintf(\"public, max-age=%d\", cfg.CacheAge))\n\t\t\t_, err := w.Write(rawSpec)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, \"Error processing JSON Swagger Spec\", http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n\n\t// Define UI Options\n\tswaggerUIOpts := middleware.SwaggerUIOpts{\n\t\tBasePath: cfg.BasePath,\n\t\tSpecURL:  specURL,\n\t\tPath:     cfg.Path,\n\t\tTitle:    cfg.Title,\n\t}\n\n\tif cfg.SwaggerURL != \"\" {\n\t\tswaggerUIOpts.SwaggerURL = cfg.SwaggerURL\n\t}\n\tif cfg.SwaggerPresetURL != \"\" {\n\t\tswaggerUIOpts.SwaggerPresetURL = cfg.SwaggerPresetURL\n\t}\n\tif cfg.SwaggerStylesURL != \"\" {\n\t\tswaggerUIOpts.SwaggerStylesURL = cfg.SwaggerStylesURL\n\t}\n\tif cfg.Favicon32 != \"\" {\n\t\tswaggerUIOpts.Favicon32 = cfg.Favicon32\n\t}\n\tif cfg.Favicon16 != \"\" {\n\t\tswaggerUIOpts.Favicon16 = cfg.Favicon16\n\t}\n\n\t// Create UI middleware\n\tmiddlewareHandler := adaptor.HTTPHandler(middleware.SwaggerUI(swaggerUIOpts, swaggerSpecHandler))\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 requests to SwaggerUI and SpecURL (swagger.json)\n                if c.Path() != swaggerUIPath && c.Path() != specURL {\n\t\t\treturn c.Next()\n\t\t}\n\n\t\t// Pass Fiber context to handler\n\t\treturn middlewareHandler(c)\n\t}\n}\n"
  },
  {
    "path": "v3/swaggerui/swagger.json",
    "content": "{\n  \"consumes\": [\n    \"application/json\"\n  ],\n  \"produces\": [\n    \"application/json\"\n  ],\n  \"schemes\": [\n    \"http\"\n  ],\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"description\": \"Documentation for TestApi\",\n    \"title\": \"TestApi\",\n    \"version\": \"1.0.0\"\n  },\n  \"basePath\": \"/\"\n}"
  },
  {
    "path": "v3/swaggerui/swagger.yaml",
    "content": "consumes:\n  - application/json\nproduces:\n  - application/json\nschemes:\n  - http\nswagger: \"2.0\"\ninfo:\n  description: \"Documentation for TestApi\"\n  title: \"TestApi\"\n  version: \"1.0.0\"\nbasePath: \"/\""
  },
  {
    "path": "v3/swaggerui/swagger_missing.json",
    "content": "{\n  \"consumes\": [\n    \"application/json\"\n  ],\n  \"produces\": [\n    \"application/json\"\n  ],\n  \"schemes\": [\n    \"http\"\n  ],\n  \"swagger\":\n  \"info\": {\n    \"description\": \"Documentation for TestApi\",\n    \"title\": \"TestApi\",\n    \"version\": \"1.0.0\"\n  },\n  \"basePath\": \"/\"\n}"
  },
  {
    "path": "v3/swaggerui/swagger_test.go",
    "content": "package swaggerui\n\nimport (\n\t_ \"embed\"\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\n//go:embed swagger.json\nvar swaggerJSON []byte\n\n//go:embed swagger.yaml\nvar swaggerYAML []byte\n\nfunc performRequest(method, target string, app *fiber.App) *http.Response {\n\tr := httptest.NewRequest(method, target, nil)\n\tresp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false})\n\treturn resp\n}\n\nfunc TestNew(t *testing.T) {\n\tt.Run(\"Endpoint check with only custom path\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tPath: \"custompath\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/custompath\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with only custom basepath\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath: \"/api/v1\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/api/v1/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/api/v1/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom config\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath: \"/\",\n\t\t\tFilePath: \"swagger.json\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom path\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath: \"/\",\n\t\t\tFilePath: \"swagger.json\",\n\t\t\tPath:     \"swagger\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/swagger\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom config and yaml spec\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath: \"/\",\n\t\t\tFilePath: \"./swagger.yaml\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.yaml\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom path and yaml spec\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath: \"/\",\n\t\t\tFilePath: \"swagger.yaml\",\n\t\t\tPath:     \"swagger\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/swagger\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.yaml\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with empty custom config\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{}\n\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with default config\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New())\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Swagger.json file is not exist\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tFilePath: \"./docs/swagger.json\",\n\t\t}\n\n\t\trequire.Panics(t, func() {\n\t\t\tapp.Use(New(cfg))\n\t\t}, \"/swagger.json file is not exist\")\n\t})\n\n\tt.Run(\"Swagger.json missing file\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tFilePath: \"./docs/swagger_missing.json\",\n\t\t}\n\n\t\trequire.Panics(t, func() {\n\t\t\tapp.Use(New(cfg))\n\t\t}, \"invalid character ':' after object key:value pair\")\n\t})\n\n\tt.Run(\"Endpoint check with multiple Swagger instances\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tBasePath: \"/api/v1\",\n\t\t}))\n\n\t\tapp.Use(New(Config{\n\t\t\tBasePath: \"/api/v2\",\n\t\t}))\n\n\t\tw1 := performRequest(\"GET\", \"/api/v1/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/api/v1/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/api/v2/docs\", app)\n\t\trequire.Equal(t, 200, w3.StatusCode)\n\n\t\tw4 := performRequest(\"GET\", \"/api/v2/swagger.json\", app)\n\t\trequire.Equal(t, 200, w4.StatusCode)\n\n\t\tw5 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w5.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom routes\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tBasePath: \"/api/v1\",\n\t\t}))\n\n\t\tapp.Get(\"/api/v1/tasks\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"success\")\n\t\t})\n\n\t\tapp.Get(\"/api/v1\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"success\")\n\t\t})\n\n\t\tw1 := performRequest(\"GET\", \"/api/v1/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/api/v1/swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\n\t\t// Verify we can send request to handler with the same BasePath as the middleware\n\t\tw4 := performRequest(\"GET\", \"/api/v1/tasks\", app)\n\t\tbodyBytes, err := io.ReadAll(w4.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, w4.StatusCode)\n\t\trequire.Equal(t, \"success\", string(bodyBytes))\n\n\t\t// Verify handler in BasePath still works\n\t\tw5 := performRequest(\"GET\", \"/api/v1\", app)\n\t\tbodyBytes, err = io.ReadAll(w5.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, w5.StatusCode)\n\t\trequire.Equal(t, \"success\", string(bodyBytes))\n\n\t\tw6 := performRequest(\"GET\", \"/api/v1/\", app)\n\t\tbodyBytes, err = io.ReadAll(w6.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, w6.StatusCode)\n\t\trequire.Equal(t, \"success\", string(bodyBytes))\n\t})\n}\n\nfunc TestNewWithFileContent(t *testing.T) {\n\tt.Run(\"Endpoint check with only custom path\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tPath:        \"custompath\",\n\t\t\tFileContent: swaggerJSON,\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/custompath\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with only custom basepath\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath:    \"/api/v1\",\n\t\t\tFileContent: swaggerJSON,\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/api/v1/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/api/v1/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom config\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath:    \"/\",\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t\tFileContent: swaggerJSON,\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom path\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath:    \"/\",\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t\tPath:        \"swagger\",\n\t\t\tFileContent: swaggerJSON,\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/swagger\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom config and yaml spec\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath:    \"/\",\n\t\t\tFilePath:    \"./doesnotexist-swagger.yaml\",\n\t\t\tFileContent: swaggerYAML,\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/doesnotexist-swagger.yaml\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom path and yaml spec\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tBasePath:    \"/\",\n\t\t\tFilePath:    \"doesnotexist-swagger.yaml\",\n\t\t\tPath:        \"swagger\",\n\t\t\tFileContent: swaggerYAML,\n\t\t}\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/swagger\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/doesnotexist-swagger.yaml\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with empty custom config\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tFileContent: swaggerJSON,\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t}\n\n\t\tapp.Use(New(cfg))\n\n\t\tw1 := performRequest(\"GET\", \"/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\t})\n\n\tt.Run(\"Swagger file content not specified\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tcfg := Config{\n\t\t\tFilePath: \"./docs/swagger.json\",\n\t\t}\n\n\t\trequire.Panics(t, func() {\n\t\t\tapp.Use(New(cfg))\n\t\t}, \"content not specified\")\n\t})\n\n\tt.Run(\"Endpoint check with multiple Swagger instances\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tBasePath:    \"/api/v1\",\n\t\t\tFileContent: swaggerJSON,\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t}))\n\n\t\tapp.Use(New(Config{\n\t\t\tBasePath:    \"/api/v2\",\n\t\t\tFileContent: swaggerJSON,\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t}))\n\n\t\tw1 := performRequest(\"GET\", \"/api/v1/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/api/v1/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/api/v2/docs\", app)\n\t\trequire.Equal(t, 200, w3.StatusCode)\n\n\t\tw4 := performRequest(\"GET\", \"/api/v2/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w4.StatusCode)\n\n\t\tw5 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w5.StatusCode)\n\t})\n\n\tt.Run(\"Endpoint check with custom routes\", func(t *testing.T) {\n\t\tapp := fiber.New()\n\n\t\tapp.Use(New(Config{\n\t\t\tBasePath:    \"/api/v1\",\n\t\t\tFileContent: swaggerJSON,\n\t\t\tFilePath:    \"doesnotexist-swagger.json\",\n\t\t}))\n\n\t\tapp.Get(\"/api/v1/tasks\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"success\")\n\t\t})\n\n\t\tapp.Get(\"/api/v1\", func(c fiber.Ctx) error {\n\t\t\treturn c.SendString(\"success\")\n\t\t})\n\n\t\tw1 := performRequest(\"GET\", \"/api/v1/docs\", app)\n\t\trequire.Equal(t, 200, w1.StatusCode)\n\n\t\tw2 := performRequest(\"GET\", \"/api/v1/doesnotexist-swagger.json\", app)\n\t\trequire.Equal(t, 200, w2.StatusCode)\n\n\t\tw3 := performRequest(\"GET\", \"/notfound\", app)\n\t\trequire.Equal(t, 404, w3.StatusCode)\n\n\t\t// Verify we can send request to handler with the same BasePath as the middleware\n\t\tw4 := performRequest(\"GET\", \"/api/v1/tasks\", app)\n\t\tbodyBytes, err := io.ReadAll(w4.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, w4.StatusCode)\n\t\trequire.Equal(t, \"success\", string(bodyBytes))\n\n\t\t// Verify handler in BasePath still works\n\t\tw5 := performRequest(\"GET\", \"/api/v1\", app)\n\t\tbodyBytes, err = io.ReadAll(w5.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, w5.StatusCode)\n\t\trequire.Equal(t, \"success\", string(bodyBytes))\n\n\t\tw6 := performRequest(\"GET\", \"/api/v1/\", app)\n\t\tbodyBytes, err = io.ReadAll(w6.Body)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 200, w6.StatusCode)\n\t\trequire.Equal(t, \"success\", string(bodyBytes))\n\t})\n}\n"
  },
  {
    "path": "v3/swaggo/README.md",
    "content": "---\nid: swaggo\n---\n\n# Swaggo\n\n> ⚠️ This module was renamed from `gofiber/swagger` to `swaggo` to clearly distinguish it from the ported `swaggerui` middleware. Update your imports accordingly.\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*swaggo*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/actions/workflows/test-swaggo.yml/badge.svg)\n\nSwaggo replaces the archived [github.com/gofiber/swagger](https://github.com/gofiber/swagger) module with an actively maintained drop-in generator for [Fiber](https://github.com/gofiber/fiber) v3. It mounts the official Swagger UI, serves the assets required by [swaggo/swag](https://github.com/swaggo/swag) generated documentation, and exposes helper utilities to wire the docs into any Fiber application.\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n### Table of Contents\n- [Signatures](#signatures)\n- [Installation](#installation)\n- [Usage](#usage)\n- [Config](#config)\n- [Default Config](#default-config)\n\n### Signatures\n```go\nvar HandlerDefault = New()\nfunc New(config ...Config) fiber.Handler\n```\n\n### Installation\nSwagger Doc Generator is tested on the latest [Go versions](https://go.dev/dl/) with support for modules. Make sure to initialize one first if you have not done that yet:\n```bash\ngo mod init github.com/<user>/<repo>\n```\nThen install the middleware:\n```bash\ngo get github.com/gofiber/contrib/v3/swaggo\n```\n\n### Usage\nFirst, document your API using swaggo/swag comments and generate the documentation files (usually inside a `docs` package) by running `swag init`.\n\n```go\npackage main\n\nimport (\n    \"github.com/gofiber/fiber/v3\"\n    swaggo \"github.com/gofiber/contrib/v3/swaggo\"\n\n    // docs are generated by Swag CLI, you have to import them.\n    // Replace with your own docs folder, usually \"github.com/username/reponame/docs\".\n    _ \"github.com/username/reponame/docs\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    // Mount the UI with the default configuration under /swagger\n    app.Get(\"/swagger/*\", swaggo.HandlerDefault)\n\n    // Customize the UI by passing a Config\n    app.Get(\"/docs/*\", swaggo.New(swaggo.Config{\n        URL:               \"http://example.com/doc.json\",\n        DeepLinking:       false,\n        DocExpansion:      \"none\",\n        OAuth2RedirectUrl: \"http://localhost:8080/swagger/oauth2-redirect.html\",\n    }))\n\n    app.Listen(\":8080\")\n}\n```\n\n### Config\n```go\ntype Config struct {\n    InstanceName              string\n    Title                     string\n    ConfigURL                 string\n    URL                       string\n    QueryConfigEnabled        bool\n    Layout                    string\n    Plugins                   []template.JS\n    Presets                   []template.JS\n    DeepLinking               bool\n    DisplayOperationId        bool\n    DefaultModelsExpandDepth  int\n    DefaultModelExpandDepth   int\n    DefaultModelRendering     string\n    DisplayRequestDuration    bool\n    DocExpansion              string\n    Filter                    FilterConfig\n    MaxDisplayedTags          int\n    ShowExtensions            bool\n    ShowCommonExtensions      bool\n    TagsSorter                template.JS\n    OnComplete                template.JS\n    SyntaxHighlight           *SyntaxHighlightConfig\n    TryItOutEnabled           bool\n    RequestSnippetsEnabled    bool\n    OAuth2RedirectUrl         string\n    RequestInterceptor        template.JS\n    RequestCurlOptions        []string\n    ResponseInterceptor       template.JS\n    ShowMutatedRequest        bool\n    SupportedSubmitMethods    []string\n    ValidatorUrl              string\n    WithCredentials           bool\n    ModelPropertyMacro        template.JS\n    ParameterMacro            template.JS\n    PersistAuthorization      bool\n    OAuth                     *OAuthConfig\n    PreauthorizeBasic         template.JS\n    PreauthorizeApiKey        template.JS\n    CustomStyle               template.CSS\n    CustomScript              template.JS\n}\n```\n\n### Default Config\n```go\nvar ConfigDefault = Config{\n    Title:       \"Swagger UI\",\n    Layout:      \"StandaloneLayout\",\n    URL:         \"doc.json\",\n    DeepLinking: true,\n    ShowMutatedRequest: true,\n    Plugins: []template.JS{\n        template.JS(\"SwaggerUIBundle.plugins.DownloadUrl\"),\n    },\n    Presets: []template.JS{\n        template.JS(\"SwaggerUIBundle.presets.apis\"),\n        template.JS(\"SwaggerUIStandalonePreset\"),\n    },\n    SyntaxHighlight: &SyntaxHighlightConfig{Activate: true, Theme: \"agate\"},\n}\n```\n\n> Refer to `config.go` for a complete list of options and documentation strings.\n\n"
  },
  {
    "path": "v3/swaggo/config.go",
    "content": "package swaggo\n\nimport (\n\t\"html/template\"\n)\n\n// Config stores SwaggerUI configuration variables\ntype Config struct {\n\t// This parameter can be used to name different swagger document instances.\n\t// default: \"\"\n\tInstanceName string `json:\"-\"`\n\n\t// Title pointing to title of HTML page.\n\t// default: \"Swagger UI\"\n\tTitle string `json:\"-\"`\n\n\t// URL to fetch external configuration document from.\n\t// default: \"\"\n\tConfigURL string `json:\"configUrl,omitempty\"`\n\n\t// The URL pointing to API definition (normally swagger.json or swagger.yaml).\n\t// default: \"doc.json\"\n\tURL string `json:\"url,omitempty\"`\n\n\t// Enables overriding configuration parameters via URL search params.\n\t// default: false\n\tQueryConfigEnabled bool `json:\"queryConfigEnabled,omitempty\"`\n\n\t// The name of a component available via the plugin system to use as the top-level layout for Swagger UI.\n\t// default: \"StandaloneLayout\"\n\tLayout string `json:\"layout,omitempty\"`\n\n\t// An array of plugin functions to use in Swagger UI.\n\t// default: [SwaggerUIBundle.plugins.DownloadUrl]\n\tPlugins []template.JS `json:\"-\"`\n\n\t// An array of presets to use in Swagger UI. Usually, you'll want to include ApisPreset if you use this option.\n\t// default: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset]\n\tPresets []template.JS `json:\"-\"`\n\n\t// If set to true, enables deep linking for tags and operations.\n\t// default: true\n\tDeepLinking bool `json:\"deepLinking\"`\n\n\t// Controls the display of operationId in operations list.\n\t// default: false\n\tDisplayOperationId bool `json:\"displayOperationId,omitempty\"`\n\n\t// The default expansion depth for models (set to -1 completely hide the models).\n\t// default: 1\n\tDefaultModelsExpandDepth int `json:\"defaultModelsExpandDepth,omitempty\"`\n\n\t// The default expansion depth for the model on the model-example section.\n\t// default: 1\n\tDefaultModelExpandDepth int `json:\"defaultModelExpandDepth,omitempty\"`\n\n\t// Controls how the model is shown when the API is first rendered.\n\t// The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links.\n\t// default: \"example\"\n\tDefaultModelRendering string `json:\"defaultModelRendering,omitempty\"`\n\n\t// Controls the display of the request duration (in milliseconds) for \"Try it out\" requests.\n\t// default: false\n\tDisplayRequestDuration bool `json:\"displayRequestDuration,omitempty\"`\n\n\t// Controls the default expansion setting for the operations and tags.\n\t// 'list' (default, expands only the tags),\n\t// 'full' (expands the tags and operations),\n\t// 'none' (expands nothing)\n\tDocExpansion string `json:\"docExpansion,omitempty\"`\n\n\t// If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown.\n\t// Can be Boolean to enable or disable, or a string, in which case filtering will be enabled using that string as the filter expression.\n\t// Filtering is case sensitive matching the filter expression anywhere inside the tag.\n\t// default: false\n\tFilter FilterConfig `json:\"-\"`\n\n\t// If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations.\n\t// default: 0\n\tMaxDisplayedTags int `json:\"maxDisplayedTags,omitempty\"`\n\n\t// Controls the display of vendor extension (x-) fields and values for Operations, Parameters, Responses, and Schema.\n\t// default: false\n\tShowExtensions bool `json:\"showExtensions,omitempty\"`\n\n\t// Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters.\n\t// default: false\n\tShowCommonExtensions bool `json:\"showCommonExtensions,omitempty\"`\n\n\t// Apply a sort to the tag list of each API. It can be 'alpha' (sort by paths alphanumerically) or a function (see Array.prototype.sort().\n\t// to learn how to write a sort function). Two tag name strings are passed to the sorter for each pass.\n\t// default: \"\" -> Default is the order determined by Swagger UI.\n\tTagsSorter template.JS `json:\"-\"`\n\n\t// Provides a mechanism to be notified when Swagger UI has finished rendering a newly provided definition.\n\t// default: \"\" -> Function=NOOP\n\tOnComplete template.JS `json:\"-\"`\n\n\t// An object with the activate and theme properties.\n\tSyntaxHighlight *SyntaxHighlightConfig `json:\"-\"`\n\n\t// Controls whether the \"Try it out\" section should be enabled by default.\n\t// default: false\n\tTryItOutEnabled bool `json:\"tryItOutEnabled,omitempty\"`\n\n\t// Enables the request snippet section. When disabled, the legacy curl snippet will be used.\n\t// default: false\n\tRequestSnippetsEnabled bool `json:\"requestSnippetsEnabled,omitempty\"`\n\n\t// OAuth redirect URL.\n\t// default: \"\"\n\tOAuth2RedirectUrl string `json:\"oauth2RedirectUrl,omitempty\"`\n\n\t// MUST be a function. Function to intercept remote definition, \"Try it out\", and OAuth 2.0 requests.\n\t// Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request.\n\t// default: \"\"\n\tRequestInterceptor template.JS `json:\"-\"`\n\n\t// If set, MUST be an array of command line options available to the curl command. This can be set on the mutated request in the requestInterceptor function.\n\t// For example request.curlOptions = [\"-g\", \"--limit-rate 20k\"]\n\t// default: nil\n\tRequestCurlOptions []string `json:\"request.curlOptions,omitempty\"`\n\n\t// MUST be a function. Function to intercept remote definition, \"Try it out\", and OAuth 2.0 responses.\n\t// Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response.\n\t// default: \"\"\n\tResponseInterceptor template.JS `json:\"-\"`\n\n\t// If set to true, uses the mutated request returned from a requestInterceptor to produce the curl command in the UI,\n\t// otherwise the request before the requestInterceptor was applied is used.\n\t// default: true\n\tShowMutatedRequest bool `json:\"showMutatedRequest\"`\n\n\t// List of HTTP methods that have the \"Try it out\" feature enabled. An empty array disables \"Try it out\" for all operations.\n\t// This does not filter the operations from the display.\n\t// Possible values are [\"get\", \"put\", \"post\", \"delete\", \"options\", \"head\", \"patch\", \"trace\"]\n\t// default: nil\n\tSupportedSubmitMethods []string `json:\"supportedSubmitMethods,omitempty\"`\n\n\t// By default, Swagger UI attempts to validate specs against swagger.io's online validator. You can use this parameter to set a different validator URL.\n\t// For example for locally deployed validators (https://github.com/swagger-api/validator-badge).\n\t// Setting it to either none, 127.0.0.1 or localhost will disable validation.\n\t// default: \"\"\n\tValidatorUrl string `json:\"validatorUrl,omitempty\"`\n\n\t// If set to true, enables passing credentials, as defined in the Fetch standard, in CORS requests that are sent by the browser.\n\t// Note that Swagger UI cannot currently set cookies cross-domain (see https://github.com/swagger-api/swagger-js/issues/1163).\n\t// as a result, you will have to rely on browser-supplied cookies (which this setting enables sending) that Swagger UI cannot control.\n\t// default: false\n\tWithCredentials bool `json:\"withCredentials,omitempty\"`\n\n\t// Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable.\n\t// default: \"\"\n\tModelPropertyMacro template.JS `json:\"-\"`\n\n\t// Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter).\n\t// Operation and parameter are objects passed for context, both remain immutable.\n\t// default: \"\"\n\tParameterMacro template.JS `json:\"-\"`\n\n\t// If set to true, it persists authorization data and it would not be lost on browser close/refresh.\n\t// default: false\n\tPersistAuthorization bool `json:\"persistAuthorization,omitempty\"`\n\n\t// Configuration information for OAuth2, optional if using OAuth2\n\tOAuth *OAuthConfig `json:\"-\"`\n\n\t// (authDefinitionKey, username, password) => action\n\t// Programmatically set values for a Basic authorization scheme.\n\t// default: \"\"\n\tPreauthorizeBasic template.JS `json:\"-\"`\n\n\t// (authDefinitionKey, apiKeyValue) => action\n\t// Programmatically set values for an API key or Bearer authorization scheme.\n\t// In case of OpenAPI 3.0 Bearer scheme, apiKeyValue must contain just the token itself without the Bearer prefix.\n\t// default: \"\"\n\tPreauthorizeApiKey template.JS `json:\"-\"`\n\n\t// Applies custom CSS styles.\n\t// default: \"\"\n\tCustomStyle template.CSS `json:\"-\"`\n\n\t// Applies custom JavaScript scripts.\n\t// default \"\"\n\tCustomScript template.JS `json:\"-\"`\n}\n\ntype FilterConfig struct {\n\tEnabled    bool\n\tExpression string\n}\n\nfunc (fc FilterConfig) Value() interface{} {\n\tif fc.Expression != \"\" {\n\t\treturn fc.Expression\n\t}\n\treturn fc.Enabled\n}\n\ntype SyntaxHighlightConfig struct {\n\t// Whether syntax highlighting should be activated or not.\n\t// default: true\n\tActivate bool `json:\"activate\"`\n\t// Highlight.js syntax coloring theme to use.\n\t// Possible values are [\"agate\", \"arta\", \"monokai\", \"nord\", \"obsidian\", \"tomorrow-night\"]\n\t// default: \"agate\"\n\tTheme string `json:\"theme,omitempty\"`\n}\n\nfunc (shc SyntaxHighlightConfig) Value() interface{} {\n\tif shc.Activate {\n\t\treturn shc\n\t}\n\treturn false\n}\n\ntype OAuthConfig struct {\n\t// ID of the client sent to the OAuth2 provider.\n\t// default: \"\"\n\tClientId string `json:\"clientId,omitempty\"`\n\n\t// Never use this parameter in your production environment.\n\t// It exposes crucial security information. This feature is intended for dev/test environments only.\n\t// Secret of the client sent to the OAuth2 provider.\n\t// default: \"\"\n\tClientSecret string `json:\"clientSecret,omitempty\"`\n\n\t// Application name, displayed in authorization popup.\n\t// default: \"\"\n\tAppName string `json:\"appName,omitempty\"`\n\n\t// Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl.\n\t// default: \"\"\n\tRealm string `json:\"realm,omitempty\"`\n\n\t// String array of initially selected oauth scopes\n\t// default: nil\n\tScopes []string `json:\"scopes,omitempty\"`\n\n\t// Additional query parameters added to authorizationUrl and tokenUrl.\n\t// default: nil\n\tAdditionalQueryStringParams map[string]string `json:\"additionalQueryStringParams,omitempty\"`\n\n\t// Unavailable\tOnly activated for the accessCode flow.\n\t// During the authorization_code request to the tokenUrl, pass the Client Password using the HTTP Basic Authentication scheme\n\t// (Authorization header with Basic base64encode(client_id + client_secret)).\n\t// default: false\n\tUseBasicAuthenticationWithAccessCodeGrant bool `json:\"useBasicAuthenticationWithAccessCodeGrant,omitempty\"`\n\n\t// Only applies to authorizationCode flows.\n\t// Proof Key for Code Exchange brings enhanced security for OAuth public clients.\n\t// default: false\n\tUsePkceWithAuthorizationCodeGrant bool `json:\"usePkceWithAuthorizationCodeGrant,omitempty\"`\n}\n\nvar (\n\tConfigDefault = Config{\n\t\tTitle:  \"Swagger UI\",\n\t\tLayout: \"StandaloneLayout\",\n\t\tPlugins: []template.JS{\n\t\t\ttemplate.JS(\"SwaggerUIBundle.plugins.DownloadUrl\"),\n\t\t},\n\t\tPresets: []template.JS{\n\t\t\ttemplate.JS(\"SwaggerUIBundle.presets.apis\"),\n\t\t\ttemplate.JS(\"SwaggerUIStandalonePreset\"),\n\t\t},\n\t\tDeepLinking:              true,\n\t\tDefaultModelsExpandDepth: 1,\n\t\tDefaultModelExpandDepth:  1,\n\t\tDefaultModelRendering:    \"example\",\n\t\tDocExpansion:             \"list\",\n\t\tSyntaxHighlight: &SyntaxHighlightConfig{\n\t\t\tActivate: true,\n\t\t\tTheme:    \"agate\",\n\t\t},\n\t\tShowMutatedRequest: true,\n\t}\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.Title == \"\" {\n\t\tcfg.Title = ConfigDefault.Title\n\t}\n\n\tif cfg.Layout == \"\" {\n\t\tcfg.Layout = ConfigDefault.Layout\n\t}\n\n\tif cfg.DefaultModelRendering == \"\" {\n\t\tcfg.DefaultModelRendering = ConfigDefault.DefaultModelRendering\n\t}\n\n\tif cfg.DocExpansion == \"\" {\n\t\tcfg.DocExpansion = ConfigDefault.DocExpansion\n\t}\n\n\tif cfg.Plugins == nil {\n\t\tcfg.Plugins = ConfigDefault.Plugins\n\t}\n\n\tif cfg.Presets == nil {\n\t\tcfg.Presets = ConfigDefault.Presets\n\t}\n\n\tif cfg.SyntaxHighlight == nil {\n\t\tcfg.SyntaxHighlight = ConfigDefault.SyntaxHighlight\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/swaggo/go.mod",
    "content": "module github.com/gofiber/contrib/v3/swaggo\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/swaggo/files/v2 v2.0.2\n\tgithub.com/swaggo/swag v1.16.6\n)\n\nrequire (\n\tgithub.com/KyleBanks/depth v1.2.1 // indirect\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/go-openapi/jsonpointer v0.23.1 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.5 // indirect\n\tgithub.com/go-openapi/spec v0.22.4 // indirect\n\tgithub.com/go-openapi/swag/conv v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/loading v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.26.0 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.26.0 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/mod v0.35.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgolang.org/x/tools v0.44.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n)\n"
  },
  {
    "path": "v3/swaggo/go.sum",
    "content": "github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=\ngithub.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=\ngithub.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=\ngithub.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=\ngithub.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=\ngithub.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=\ngithub.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=\ngithub.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=\ngithub.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=\ngithub.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=\ngithub.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=\ngithub.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=\ngithub.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=\ngithub.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=\ngithub.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=\ngithub.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=\ngithub.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=\ngithub.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=\ngithub.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=\ngithub.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=\ngithub.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=\ngithub.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=\ngithub.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=\ngithub.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=\ngithub.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=\ngithub.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=\ngithub.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=\ngithub.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=\ngithub.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=\ngolang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=\ngolang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/swaggo/index.go",
    "content": "package swaggo\n\nconst indexTmpl string = `\n<!-- HTML for static distribution bundle build -->\n<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>{{.Title}}</title>\n    <link href=\"https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"./swagger-ui.css\" >\n    <link rel=\"icon\" type=\"image/png\" href=\"./favicon-32x32.png\" sizes=\"32x32\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"./favicon-16x16.png\" sizes=\"16x16\" />\n    {{- if .CustomStyle}}\n      <style>\n        body { margin: 0; }\n        {{.CustomStyle}}\n      </style>\n    {{- end}}\n    {{- if .CustomScript}}\n      <script>\n        {{.CustomScript}}\n      </script>\n    {{- end}}\n  </head>\n  <body>\n    <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" style=\"position:absolute;width:0;height:0\">\n      <defs>\n        <symbol viewBox=\"0 0 20 20\" id=\"unlocked\">\n              <path d=\"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z\"></path>\n        </symbol>\n        <symbol viewBox=\"0 0 20 20\" id=\"locked\">\n          <path d=\"M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z\"/>\n        </symbol>\n        <symbol viewBox=\"0 0 20 20\" id=\"close\">\n          <path d=\"M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z\"/>\n        </symbol>\n        <symbol viewBox=\"0 0 20 20\" id=\"large-arrow\">\n          <path d=\"M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z\"/>\n        </symbol>\n        <symbol viewBox=\"0 0 20 20\" id=\"large-arrow-down\">\n          <path d=\"M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z\"/>\n        </symbol>\n        <symbol viewBox=\"0 0 24 24\" id=\"jump-to\">\n          <path d=\"M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z\"/>\n        </symbol>\n        <symbol viewBox=\"0 0 24 24\" id=\"expand\">\n          <path d=\"M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z\"/>\n        </symbol>\n      </defs>\n    </svg>\n    <div id=\"swagger-ui\"></div>\n    <script src=\"./swagger-ui-bundle.js\"> </script>\n    <script src=\"./swagger-ui-standalone-preset.js\"> </script>\n    <script>\n    window.onload = function() {\n      config = {{.}};\n      config.dom_id = '#swagger-ui';\n      config.plugins = [\n        {{- range $plugin := .Plugins }}\n          {{$plugin}},\n        {{- end}}\n      ];\n      config.presets = [\n        {{- range $preset := .Presets }}\n          {{$preset}},\n        {{- end}}\n      ];\n      config.filter = {{.Filter.Value}}\n      config.syntaxHighlight = {{.SyntaxHighlight.Value}}\n      {{if .TagsSorter}}\n        config.tagsSorter = {{.TagsSorter}}\n      {{end}}\n      {{if .OnComplete}}\n        config.onComplete = {{.OnComplete}}\n      {{end}}\n      {{if .RequestInterceptor}}\n        config.requestInterceptor = {{.RequestInterceptor}}\n      {{end}}\n      {{if .ResponseInterceptor}}\n        config.responseInterceptor = {{.ResponseInterceptor}}\n      {{end}}\n      {{if .ModelPropertyMacro}}\n        config.modelPropertyMacro = {{.ModelPropertyMacro}}\n      {{end}}\n      {{if .ParameterMacro}}\n        config.parameterMacro = {{.ParameterMacro}}\n      {{end}}\n\n      const ui = SwaggerUIBundle(config);\n\n      {{if .OAuth}}\n        ui.initOAuth({{.OAuth}});\n      {{end}}\n      {{if .PreauthorizeBasic}}\n        ui.preauthorizeBasic({{.PreauthorizeBasic}});\n      {{end}}\n      {{if .PreauthorizeApiKey}}\n        ui.preauthorizeApiKey({{.PreauthorizeApiKey}});\n      {{end}}\n\n      window.ui = ui\n    }\n    </script>\n  </body>\n</html>\n`\n"
  },
  {
    "path": "v3/swaggo/swagger.go",
    "content": "package swaggo\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"io\"\n\t\"io/fs\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\tswaggerFiles \"github.com/swaggo/files/v2\"\n\t\"github.com/swaggo/swag\"\n)\n\nconst (\n\tdefaultDocURL = \"doc.json\"\n\tdefaultIndex  = \"index.html\"\n)\n\nvar HandlerDefault = New()\n\n// New returns custom handler\nfunc New(config ...Config) fiber.Handler {\n\tcfg := configDefault(config...)\n\n\tindex, err := template.New(\"swagger_index.html\").Parse(indexTmpl)\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"fiber: swagger middleware error -> %w\", err))\n\t}\n\n\tvar (\n\t\tbasePrefix string\n\t\tonce       sync.Once\n\t)\n\n\treturn func(c fiber.Ctx) error {\n\t\tonce.Do(func() {\n\t\t\tbasePrefix = strings.ReplaceAll(c.Route().Path, \"*\", \"\")\n\t\t})\n\n\t\tprefix := basePrefix\n\t\tif forwardedPrefix := getForwardedPrefix(c); forwardedPrefix != \"\" {\n\t\t\tprefix = forwardedPrefix + prefix\n\t\t}\n\n\t\tcfgCopy := cfg\n\t\tif len(cfgCopy.URL) == 0 {\n\t\t\tcfgCopy.URL = path.Join(prefix, defaultDocURL)\n\t\t}\n\n\t\tp := utils.CopyString(c.Params(\"*\"))\n\n\t\tswitch p {\n\t\tcase defaultIndex:\n\t\t\tc.Type(\"html\")\n\t\t\treturn index.Execute(c, cfgCopy)\n\t\tcase defaultDocURL:\n\t\t\tvar doc string\n\t\t\tif doc, err = swag.ReadDoc(cfgCopy.InstanceName); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn c.Type(\"json\").SendString(doc)\n\t\tcase \"\", \"/\":\n\t\t\treturn c.Redirect().Status(fiber.StatusMovedPermanently).To(path.Join(prefix, defaultIndex))\n\t\tdefault:\n\t\t\tfilePath := path.Clean(\"/\" + p)\n\t\t\tfilePath = strings.TrimPrefix(filePath, \"/\")\n\t\t\tif filePath == \"\" {\n\t\t\t\treturn fiber.ErrNotFound\n\t\t\t}\n\n\t\t\tfile, err := swaggerFiles.FS.Open(filePath)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\treturn fiber.ErrNotFound\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tcerr := file.Close()\n\t\t\t\tif cerr != nil && err == nil {\n\t\t\t\t\terr = cerr\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tinfo, err := file.Stat()\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\treturn fiber.ErrNotFound\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif info.IsDir() {\n\t\t\t\treturn fiber.ErrNotFound\n\t\t\t}\n\n\t\t\tdata, err := io.ReadAll(file)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif ext := strings.TrimPrefix(path.Ext(filePath), \".\"); ext != \"\" {\n\t\t\t\tc.Type(ext)\n\t\t\t}\n\n\t\t\treturn c.Send(data)\n\t\t}\n\t}\n}\n\nfunc getForwardedPrefix(c fiber.Ctx) string {\n\theader := c.GetReqHeaders()[\"X-Forwarded-Prefix\"]\n\n\tif len(header) == 0 {\n\t\treturn \"\"\n\t}\n\n\tprefix := \"\"\n\n\tfor _, rawPrefix := range header {\n\t\tendIndex := len(rawPrefix)\n\t\tfor endIndex > 1 && rawPrefix[endIndex-1] == '/' {\n\t\t\tendIndex--\n\t\t}\n\n\t\tprefix += rawPrefix[:endIndex]\n\t}\n\n\treturn prefix\n}\n"
  },
  {
    "path": "v3/swaggo/swagger_test.go",
    "content": "package swaggo\n\nimport (\n\t\"mime\"\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/swaggo/swag\"\n)\n\ntype mockedSwag struct{}\n\nfunc (s *mockedSwag) ReadDoc() string {\n\treturn `{\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"This is a sample server.\",\n        \"title\": \"Swagger Example API\",\n        \"termsOfService\": \"http://swagger.io/terms/\",\n        \"contact\": {\n            \"name\": \"API Support\",\n            \"url\": \"http://www.swagger.io/support\",\n            \"email\": \"support@swagger.io\"\n        },\n        \"license\": {\n            \"name\": \"Apache 2.0\",\n            \"url\": \"http://www.apache.org/licenses/LICENSE-2.0.html\"\n        },\n        \"version\": \"1.0\"\n    },\n    \"host\": \"petstore.swagger.io\",\n    \"basePath\": \"/v2\",\n    \"paths\": {}\n}`\n}\n\nvar (\n\tregistrationOnce sync.Once\n)\n\nfunc Test_Swagger(t *testing.T) {\n\tapp := fiber.New()\n\n\tregistrationOnce.Do(func() {\n\t\tswag.Register(swag.Name, &mockedSwag{})\n\t})\n\n\tapp.Get(\"/swag/*\", HandlerDefault)\n\n\ttests := []struct {\n\t\tname        string\n\t\turl         string\n\t\tstatusCode  int\n\t\tcontentType string\n\t\tlocation    string\n\t}{\n\t\t{\n\t\t\tname:        \"Should be returns status 200 with 'text/html' content-type\",\n\t\t\turl:         \"/swag/index.html\",\n\t\t\tstatusCode:  200,\n\t\t\tcontentType: \"text/html\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Should be returns status 200 with 'application/json' content-type\",\n\t\t\turl:         \"/swag/doc.json\",\n\t\t\tstatusCode:  200,\n\t\t\tcontentType: \"application/json\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Should be returns status 200 with 'image/png' content-type\",\n\t\t\turl:         \"/swag/favicon-16x16.png\",\n\t\t\tstatusCode:  200,\n\t\t\tcontentType: \"image/png\",\n\t\t},\n\t\t{\n\t\t\tname:       \"Should return status 301\",\n\t\t\turl:        \"/swag/\",\n\t\t\tstatusCode: 301,\n\t\t\tlocation:   \"/swag/index.html\",\n\t\t},\n\t\t{\n\t\t\tname:       \"Should return status 404\",\n\t\t\turl:        \"/swag/notfound\",\n\t\t\tstatusCode: 404,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treq, err := http.NewRequest(http.MethodGet, tt.url, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treq.Host = \"localhost\"\n\n\t\t\tresp, err := app.Test(req)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif resp.StatusCode != tt.statusCode {\n\t\t\t\tt.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tif tt.contentType != \"\" {\n\t\t\t\tct := resp.Header.Get(\"Content-Type\")\n\t\t\t\tmediaType, _, err := mime.ParseMediaType(ct)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"invalid content type %q: %v\", ct, err)\n\t\t\t\t}\n\t\t\t\tif mediaType != tt.contentType {\n\t\t\t\t\tt.Fatalf(`Content-Type: got %s - expected %s`, mediaType, tt.contentType)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.location != \"\" {\n\t\t\t\tlocation := resp.Header.Get(\"Location\")\n\t\t\t\tif location != tt.location {\n\t\t\t\t\tt.Fatalf(`Location: got %s - expected %s`, location, tt.location)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Swagger_Proxy_Redirect(t *testing.T) {\n\tapp := fiber.New()\n\n\tregistrationOnce.Do(func() {\n\t\tswag.Register(swag.Name, &mockedSwag{})\n\t})\n\n\t// Use new handler since the prefix is created only once per handler\n\tapp.Get(\"/swag/*\", New())\n\n\tstatusCode := 301\n\tlocation := \"/custom/path/swag/index.html\"\n\n\tt.Run(\"Should return status 301 with proxy redirect\", func(t *testing.T) {\n\t\treq, err := http.NewRequest(http.MethodGet, \"/swag/\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\treq.Host = \"localhost\"\n\t\treq.Header.Set(\"X-Forwarded-Prefix\", \"/custom/path/\")\n\n\t\tresp, err := app.Test(req)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif resp.StatusCode != statusCode {\n\t\t\tt.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, statusCode)\n\t\t}\n\n\t\tif location != \"\" {\n\t\t\tresponseLocation := resp.Header.Get(\"Location\")\n\t\t\tif responseLocation != location {\n\t\t\t\tt.Fatalf(`Location: got %s - expected %s`, responseLocation, location)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "v3/testcontainers/README.md",
    "content": "---\nid: testcontainers\n---\n\n# Testcontainers\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*testcontainers*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20Testcontainers%20Services/badge.svg)\n\nA [Testcontainers](https://golang.testcontainers.org/) Service Implementation for Fiber.\n\n:::note\n\nRequires Go **1.25** and above\n\n:::\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n> Test requirement: integration tests for this package require a reachable Docker daemon.\n\n## Common Use Cases\n\n- Local development\n- Integration testing\n- Isolated service testing\n- End-to-end testing\n\n## Install\n\n:::caution\n\nThis Service Implementation only supports Fiber **v3**.\n\n:::\n\n```shell\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/testcontainers\n```\n\n## Signature\n\n### NewModuleConfig\n\n```go\n// NewModuleConfig creates a new container service config for a module.\n//\n// - The serviceKey is the key used to identify the service in the Fiber app's state.\n// - The img is the image name to use for the container.\n// - The run is the function to use to run the container. It's usually the Run function from the module, like [redis.Run] or [postgres.Run].\n// - The opts are the functional options to pass to the run function. This argument is optional.\nfunc NewModuleConfig[T testcontainers.Container](\n serviceKey string,\n img string,\n run func(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (T, error),\n opts ...testcontainers.ContainerCustomizer,\n) Config[T] {\n```\n\n### NewContainerConfig\n\n```go\n// NewContainerConfig creates a new container service config for a generic container type,\n// not created by a Testcontainers module. So this function best used in combination with\n// the [AddService] function to add a custom container to the Fiber app's state.\n//\n// - The serviceKey is the key used to identify the service in the Fiber app's state.\n// - The img is the image name to use for the container.\n// - The opts are the functional options to pass to the [testcontainers.Run] function. This argument is optional.\n//\n// This function uses the [testcontainers.Run] function as the run function.\nfunc NewContainerConfig[T *testcontainers.DockerContainer](serviceKey string, img string, opts ...testcontainers.ContainerCustomizer) Config[*testcontainers.DockerContainer]\n```\n\n### AddService\n\n```go\n// AddService adds a Testcontainers container as a [fiber.Service] for the Fiber app.\n// It returns a pointer to a [ContainerService[T]] object, which contains the key used to identify\n// the service in the Fiber app's state, and an error if the config is nil.\n// The container should be a function like redis.Run or postgres.Run that returns a container type\n// which embeds [testcontainers.Container].\n// - The cfg is the Fiber app's configuration, needed to add the service to the Fiber app's state.\n// - The containerConfig is the configuration for the container, where:\n//   - The containerConfig.ServiceKey is the key used to identify the service in the Fiber app's state.\n//   - The containerConfig.Run is the function to use to run the container. It's usually the Run function from the module, like redis.Run or postgres.Run.\n//   - The containerConfig.Image is the image to use for the container.\n//   - The containerConfig.Options are the functional options to pass to the [testcontainers.Run] function. This argument is optional.\n//\n// Use [NewModuleConfig] or [NewContainerConfig] helper functions to create valid containerConfig objects.\nfunc AddService[T testcontainers.Container](cfg *fiber.Config, containerConfig Config[T]) (*ContainerService[T], error) {\n```\n\n## Types\n\n### Config\n\nThe `Config` type is a generic type that is used to configure the container.\n\n| Property    | Type | Description | Default |\n|-------------|------|-------------|---------|\n| ServiceKey  | string | The key used to identify the service in the Fiber app's state. | - |\n| Image      | string | The image name to use for the container. | - |\n| Run     | func(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (T, error) | The function to use to run the container. It's usually the Run function from the testcontainers-go module, like redis.Run or postgres.Run | - |\n| Options    | []testcontainers.ContainerCustomizer | The functional options to pass to the [testcontainers.Run] function. This argument is optional. | - |\n\n```go\n// Config contains the configuration for a container service.\ntype Config[T testcontainers.Container] struct {\n // ServiceKey is the key used to identify the service in the Fiber app's state.\n ServiceKey string\n\n // Image is the image name to use for the container.\n Image string\n\n // Run is the function to use to run the container.\n // It's usually the Run function from the testcontainers-go module, like redis.Run or postgres.Run,\n // although it could be the generic [testcontainers.Run] function from the testcontainers-go package.\n Run func(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (T, error)\n\n // Options are the functional options to pass to the [testcontainers.Run] function. This argument is optional.\n // You can find the available options in the [testcontainers website].\n //\n // [testcontainers website]: https://golang.testcontainers.org/features/creating_container/#customizing-the-container\n Options []testcontainers.ContainerCustomizer\n}\n```\n\n### ContainerService\n\nThe `ContainerService` type is a generic type that embeds a [testcontainers.Container](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#Container) interface,\nand implements the [fiber.Service] interface, thanks to the Start, String, State and Terminate methods. It manages the lifecycle of a `testcontainers.Container` instance,\nand it can be retrieved from the Fiber app's state calling the `fiber.MustGetService` function with the key returned by the `ContainerService.Key` method.\n\nThe type parameter `T` must implement the [testcontainers.Container](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#Container) interface,\nas in the Testcontainers Go modules (e.g. [redis.RedisContainer](https://pkg.go.dev/github.com/testcontainers/testcontainers-go/modules/redis#RedisContainer),\n[postgres.PostgresContainer](https://pkg.go.dev/github.com/testcontainers/testcontainers-go/modules/postgres#PostgresContainer), etc.), or in the generic\n[testcontainers.DockerContainer](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#GenericContainer) type, used for custom containers.\n\n:::note\n\nSince `ContainerService` implements the `fiber.Service` interface, container cleanup is handled automatically by the Fiber framework when the application shuts down. There's no need for manual cleanup code.\n\n:::\n\n```go\ntype ContainerService[T testcontainers.Container] struct\n```\n\n#### Signature\n\n#####  Key\n\n```go\n// Key returns the key used to identify the service in the Fiber app's state.\n// Consumers should use string constants for service keys to ensure consistency\n// when retrieving services from the Fiber app's state.\nfunc (c *ContainerService[T]) Key() string\n```\n\n##### Container\n\n```go\n// Container returns the Testcontainers container instance, giving full access to the T type methods.\n// It's useful to access the container's methods, like [testcontainers.Container.MappedPort]\n// or [testcontainers.Container.Inspect].\nfunc (c *ContainerService[T]) Container() T\n```\n\n##### Start\n\n```go\n// Start creates and starts the container, calling the [run] function with the [img] and [opts] arguments.\n// It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) Start(ctx context.Context) error\n```\n\n##### String\n\n```go\n// String returns the service key, which uniquely identifies the container service.\n// It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) String() string\n```\n\n##### State\n\n```go\n// State returns the status of the container.\n// It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) State(ctx context.Context) (string, error)\n```\n\n##### Terminate\n\n```go\n// Terminate stops and removes the container. It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) Terminate(ctx context.Context) error\n```\n\n### Common Errors\n\n| Error | Description | Resolution |\n|-------|-------------|------------|\n| ErrNilConfig | Returned when the config is nil | Ensure config is properly initialized |\n| ErrContainerNotRunning | Returned when the container is not running | Check container state before operations |\n| ErrEmptyServiceKey | Returned when the service key is empty | Provide a non-empty service key |\n| ErrImageEmpty | Returned when the image is empty | Provide a valid image name |\n| ErrRunNil | Returned when the run is nil | Provide a valid run function |\n\n## Examples\n\nYou can find more examples in the [testable examples](https://github.com/gofiber/contrib/blob/main/v3/testcontainers/examples_test.go).\n\n### Adding a module container using the Testcontainers Go's Redis module\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"log\"\n\n \"github.com/gofiber/fiber/v3\"\n\n \"github.com/gofiber/contrib/v3/testcontainers\"\n tc \"github.com/testcontainers/testcontainers-go\"\n \"github.com/testcontainers/testcontainers-go/modules/redis\"\n)\n\nfunc main() {\n cfg := &fiber.Config{}\n\n // Define the base key for the module service.\n // The service returned by the [testcontainers.AddService] function,\n // using the [ContainerService.Key] method,\n // concatenates the base key with the \"using testcontainers-go\" suffix.\n const (\n  redisKey    = \"redis-module\"\n )\n\n // Adding containers coming from the testcontainers-go modules,\n // in this case, a Redis and a Postgres container.\n\n redisModuleConfig := testcontainers.NewModuleConfig(redisKey, \"redis:latest\", redis.Run)\n redisSrv, err := testcontainers.AddService(cfg, redisModuleConfig)\n if err != nil {\n  log.Println(\"error adding redis module:\", err)\n  return\n }\n\n // Create a new Fiber app, using the provided configuration.\n app := fiber.New(*cfg)\n\n // Retrieve all services from the app's state.\n // This returns a slice of all the services registered in the app's state.\n srvs := app.State().Services()\n\n // Retrieve the Redis container from the app's state using the key returned by the [ContainerService.Key] method.\n redisCtr := fiber.MustGetService[*testcontainers.ContainerService[*redis.RedisContainer]](app.State(), redisSrv.Key())\n\n // Start the Fiber app.\n app.Listen(\":3000\")\n}\n```\n\n### Adding a custom container using the Testcontainers Go package\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"log\"\n\n \"github.com/gofiber/fiber/v3\"\n\n \"github.com/gofiber/contrib/v3/testcontainers\"\n tc \"github.com/testcontainers/testcontainers-go\"\n)\n\nfunc main() {\n cfg := &fiber.Config{}\n\n // Define the base key for the generic service.\n // The service returned by the [testcontainers.AddService] function,\n // using the [ContainerService.Key] method,\n // concatenates the base key with the \"using testcontainers-go\" suffix.\n const (\n  nginxKey = \"nginx-generic\"\n )\n\n // Adding a generic container, directly from the testcontainers-go package.\n containerConfig := testcontainers.NewContainerConfig(nginxKey, \"nginx:latest\", tc.WithExposedPorts(\"80/tcp\"))\n\n nginxSrv, err := testcontainers.AddService(cfg, containerConfig)\n if err != nil {\n  log.Println(\"error adding nginx generic:\", err)\n  return\n }\n\n app := fiber.New(*cfg)\n\n nginxCtr := fiber.MustGetService[*testcontainers.ContainerService[*tc.DockerContainer]](app.State(), nginxSrv.Key())\n\n // Start the Fiber app.\n app.Listen(\":3000\")\n}\n```\n"
  },
  {
    "path": "v3/testcontainers/config.go",
    "content": "package testcontainers\n\nimport (\n\t\"context\"\n\n\ttc \"github.com/testcontainers/testcontainers-go\"\n)\n\n// Config contains the configuration for a container service.\ntype Config[T tc.Container] struct {\n\t// ServiceKey is the key used to identify the service in the Fiber app's state.\n\tServiceKey string\n\n\t// Image is the image name to use for the container.\n\tImage string\n\n\t// Run is the function to use to run the container.\n\t// It's usually the Run function from the testcontainers-go module, like redis.Run or postgres.Run,\n\t// although it could be the generic [tc.Run] function from the testcontainers-go package.\n\tRun func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (T, error)\n\n\t// Options are the functional options to pass to the [tc.Run] function. This argument is optional.\n\t// You can find the available options in the [testcontainers website].\n\t//\n\t// [testcontainers website]: https://golang.tc.org/features/creating_container/#customizing-the-container\n\tOptions []tc.ContainerCustomizer\n}\n\n// NewModuleConfig creates a new container service config for a module.\n//\n// - The serviceKey is the key used to identify the service in the Fiber app's state.\n// - The img is the image name to use for the container.\n// - The run is the function to use to run the container. It's usually the Run function from the module, like [redis.Run] or [postgres.Run].\n// - The opts are the functional options to pass to the run function. This argument is optional.\nfunc NewModuleConfig[T tc.Container](\n\tserviceKey string,\n\timg string,\n\trun func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (T, error),\n\topts ...tc.ContainerCustomizer,\n) Config[T] {\n\n\treturn Config[T]{\n\t\tServiceKey: serviceKey,\n\t\tImage:      img,\n\t\tRun:        run,\n\t\tOptions:    opts,\n\t}\n}\n\n// NewContainerConfig creates a new container service config for a generic container type,\n// not created by a Testcontainers module. So this function best used in combination with\n// the [AddService] function to add a custom container to the Fiber app's state.\n//\n// - The serviceKey is the key used to identify the service in the Fiber app's state.\n// - The img is the image name to use for the container.\n// - The opts are the functional options to pass to the [tc.Run] function. This argument is optional.\n//\n// This function uses the [tc.Run] function as the run function.\nfunc NewContainerConfig(serviceKey string, img string, opts ...tc.ContainerCustomizer) Config[*tc.DockerContainer] {\n\treturn NewModuleConfig(serviceKey, img, tc.Run, opts...)\n}\n"
  },
  {
    "path": "v3/testcontainers/examples_test.go",
    "content": "package testcontainers_test\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\n\t\"github.com/gofiber/contrib/v3/testcontainers\"\n\ttc \"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/modules/postgres\"\n\t\"github.com/testcontainers/testcontainers-go/modules/redis\"\n)\n\nfunc ExampleAddService_fromContainer() {\n\tcfg := &fiber.Config{}\n\n\t// Define the base key for the generic service.\n\t// The service returned by the [testcontainers.AddService] function,\n\t// using the [ContainerService.Key] method,\n\t// concatenates the base key with the \"using testcontainers-go\" suffix.\n\tconst (\n\t\tnginxKey = \"nginx-generic\"\n\t)\n\n\t// Adding a generic container, directly from the testcontainers-go package.\n\tcontainerConfig := testcontainers.NewContainerConfig(nginxKey, \"nginx:latest\", tc.WithExposedPorts(\"80/tcp\"))\n\n\tnginxSrv, err := testcontainers.AddService(cfg, containerConfig)\n\tif err != nil {\n\t\tlog.Println(\"error adding nginx generic:\", err)\n\t\treturn\n\t}\n\n\tapp := fiber.New(*cfg)\n\tfmt.Println(app.State().ServicesLen())\n\n\tsrvs := app.State().Services()\n\tfmt.Println(len(srvs))\n\n\tnginxCtr := fiber.MustGetService[*testcontainers.ContainerService[*tc.DockerContainer]](app.State(), nginxSrv.Key())\n\n\tfmt.Println(nginxCtr.String())\n\n\t// Output:\n\t// 1\n\t// 1\n\t// nginx-generic (using testcontainers-go)\n}\n\nfunc ExampleAddService_fromModule() {\n\tcfg := &fiber.Config{}\n\n\t// Define the base keys for the module services.\n\t// The service returned by the [testcontainers.AddService] function,\n\t// using the [ContainerService.Key] method,\n\t// concatenates the base key with the \"using testcontainers-go\" suffix.\n\tconst (\n\t\tredisKey    = \"redis-module\"\n\t\tpostgresKey = \"postgres-module\"\n\t)\n\n\t// Adding containers coming from the testcontainers-go modules,\n\t// in this case, a Redis and a Postgres container.\n\n\tredisModuleConfig := testcontainers.NewModuleConfig(redisKey, \"redis:latest\", redis.Run)\n\tredisSrv, err := testcontainers.AddService(cfg, redisModuleConfig)\n\tif err != nil {\n\t\tlog.Println(\"error adding redis module:\", err)\n\t\treturn\n\t}\n\n\tpostgresModuleConfig := testcontainers.NewModuleConfig(postgresKey, \"postgres:latest\", postgres.Run)\n\tpostgresSrv, err := testcontainers.AddService(cfg, postgresModuleConfig)\n\tif err != nil {\n\t\tlog.Println(\"error adding postgres module:\", err)\n\t\treturn\n\t}\n\n\t// Create a new Fiber app, using the provided configuration.\n\tapp := fiber.New(*cfg)\n\n\t// Verify the number of services in the app's state.\n\tfmt.Println(app.State().ServicesLen())\n\n\t// Retrieve all services from the app's state.\n\t// This returns a slice of all the services registered in the app's state.\n\tsrvs := app.State().Services()\n\tfmt.Println(len(srvs))\n\n\t// Retrieve the Redis container from the app's state using the key returned by the [ContainerService.Key] method.\n\tredisCtr := fiber.MustGetService[*testcontainers.ContainerService[*redis.RedisContainer]](app.State(), redisSrv.Key())\n\n\t// Retrieve the Postgres container from the app's state using the key returned by the [ContainerService.Key] method.\n\tpostgresCtr := fiber.MustGetService[*testcontainers.ContainerService[*postgres.PostgresContainer]](app.State(), postgresSrv.Key())\n\n\t// Verify the string representation of the Redis and Postgres containers.\n\tfmt.Println(redisCtr.String())\n\tfmt.Println(postgresCtr.String())\n\n\t// Output:\n\t// 2\n\t// 2\n\t// redis-module (using testcontainers-go)\n\t// postgres-module (using testcontainers-go)\n}\n"
  },
  {
    "path": "v3/testcontainers/go.mod",
    "content": "module github.com/gofiber/contrib/v3/testcontainers\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/testcontainers/testcontainers-go v0.42.0\n)\n\n// Test-time dependencies\nrequire (\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/testcontainers/testcontainers-go/modules/postgres v0.42.0\n\tgithub.com/testcontainers/testcontainers-go/modules/redis v0.42.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v1.0.0-rc.4 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.7.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/mdelapenya/tlscert v0.2.0 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/moby/api v1.54.1 // indirect\n\tgithub.com/moby/moby/client v0.4.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.3 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect\n\tgo.opentelemetry.io/otel v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.43.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/testcontainers/go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=\ngithub.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=\ngithub.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=\ngithub.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=\ngithub.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=\ngithub.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=\ngithub.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=\ngithub.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=\ngithub.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=\ngithub.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=\ngithub.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=\ngithub.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=\ngithub.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=\ngithub.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\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/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=\ngithub.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=\ngithub.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo=\ngithub.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs=\ngithub.com/testcontainers/testcontainers-go/modules/redis v0.42.0 h1:id/6LH8ZeDrtAUVSuNvZUAJ1kVpb82y1pr9yweAWsRg=\ngithub.com/testcontainers/testcontainers-go/modules/redis v0.42.0/go.mod h1:uF0jI8FITagQpBNOgweGBmPf6rP4K0SeL1XFPbsZSSY=\ngithub.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=\ngo.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=\ngo.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=\ngo.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=\ngo.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=\ngo.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=\ngo.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=\ngo.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=\ngo.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=\ngolang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "v3/testcontainers/testcontainers.go",
    "content": "package testcontainers\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\ttc \"github.com/testcontainers/testcontainers-go\"\n)\n\nconst (\n\t// serviceSuffix is the suffix added to the service key to identify it as a Testcontainers service.\n\tserviceSuffix = \" (using testcontainers-go)\"\n\n\t// fiberContainerLabel is the label added to the container to identify it as a Fiber app.\n\tfiberContainerLabel = \"org.testcontainers.golang.framework\"\n\n\t// fiberContainerLabelValue is the value of the label added to the container to identify it as a Fiber app.\n\tfiberContainerLabelValue = \"Go Fiber\"\n)\n\nvar (\n\t// ErrNilConfig is returned when the config is nil.\n\tErrNilConfig = errors.New(\"config is nil\")\n\n\t// ErrContainerNotRunning is returned when the container is not running.\n\tErrContainerNotRunning = errors.New(\"container is not running\")\n\n\t// ErrEmptyServiceKey is returned when the service key is empty.\n\tErrEmptyServiceKey = errors.New(\"service key is empty\")\n\n\t// ErrImageEmpty is returned when the image is empty.\n\tErrImageEmpty = errors.New(\"image is empty\")\n\n\t// ErrRunNil is returned when the run is nil.\n\tErrRunNil = errors.New(\"run is nil\")\n)\n\n// buildKey builds a key for a container service.\n// This key is used to identify the service in the Fiber app's state.\nfunc buildKey(key string) string {\n\tif strings.HasSuffix(key, serviceSuffix) {\n\t\treturn key\n\t}\n\n\treturn key + serviceSuffix\n}\n\n// ContainerService represents a container that implements the [fiber.Service] interface.\n// It manages the lifecycle of a [tc.Container] instance, and it can be\n// retrieved from the Fiber app's state calling the [fiber.MustGetService] function with\n// the key returned by the [ContainerService.Key] method.\n//\n// The type parameter T must implement the [tc.Container] interface.\ntype ContainerService[T tc.Container] struct {\n\t// The container instance, using the generic type T.\n\tctr T\n\n\t// initialized tracks whether the container has been started\n\tinitialized bool\n\n\t// The key used to identify the service in the Fiber app's state.\n\tkey string\n\n\t// The image to use for the container.\n\t// It's used to run the container with a specific image.\n\timg string\n\n\t// The functional options to pass to the [run] function.\n\t// It's used to customize the container.\n\topts []tc.ContainerCustomizer\n\n\t// The function to use to run the container.\n\t// It's usually the Run function from a testcontainers-go module, like redis.Run or postgres.Run,\n\t// or the Run function from the testcontainers-go package.\n\t// It returns a container instance of type T, which embeds [tc.Container],\n\t// like [redis.RedisContainer] or [postgres.PostgresContainer].\n\trun func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (T, error)\n}\n\n// Key returns the key used to identify the service in the Fiber app's state.\n// Consumers should use string constants for service keys to ensure consistency\n// when retrieving services from the Fiber app's state.\nfunc (c *ContainerService[T]) Key() string {\n\treturn c.key\n}\n\n// Container returns the Testcontainers container instance, giving full access to the T type methods.\n// It's useful to access the container's methods, like [tc.Container.MappedPort]\n// or [tc.Container.Inspect].\nfunc (c *ContainerService[T]) Container() T {\n\tif !c.initialized {\n\t\tvar zero T\n\t\treturn zero\n\t}\n\n\treturn c.ctr\n}\n\n// Start creates and starts the container, calling the [run] function with the [img] and [opts] arguments.\n// It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) Start(ctx context.Context) error {\n\tif c.initialized {\n\t\treturn fmt.Errorf(\"container %s already initialized\", c.key)\n\t}\n\n\topts := append([]tc.ContainerCustomizer{}, c.opts...)\n\topts = append(opts, tc.WithLabels(map[string]string{\n\t\tfiberContainerLabel: fiberContainerLabelValue,\n\t}))\n\n\tctr, err := c.run(ctx, c.img, opts...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"run container: %w\", err)\n\t}\n\n\tc.ctr = ctr\n\tc.initialized = true\n\n\treturn nil\n}\n\n// String returns the service key, which uniquely identifies the container service.\n// It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) String() string {\n\treturn c.key\n}\n\n// State returns the status of the container.\n// It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) State(ctx context.Context) (string, error) {\n\tif !c.initialized {\n\t\treturn \"\", ErrContainerNotRunning\n\t}\n\n\tst, err := c.ctr.State(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"get container state for %s: %w\", c.key, err)\n\t}\n\n\tif st == nil {\n\t\treturn \"\", fmt.Errorf(\"container state is nil for %s\", c.key)\n\t}\n\n\treturn string(st.Status), nil\n}\n\n// Terminate stops and removes the container. It implements the [fiber.Service] interface.\nfunc (c *ContainerService[T]) Terminate(ctx context.Context) error {\n\tif !c.initialized {\n\t\treturn ErrContainerNotRunning\n\t}\n\n\tif err := c.ctr.Terminate(ctx); err != nil {\n\t\treturn fmt.Errorf(\"terminate container: %w\", err)\n\t}\n\n\tc.initialized = false\n\t// Reset container reference to avoid potential use after free\n\tvar zero T\n\tc.ctr = zero\n\n\treturn nil\n}\n\n// AddService adds a Testcontainers container as a [fiber.Service] for the Fiber app.\n// It returns a pointer to a [ContainerService[T]] object, which contains the key used to identify\n// the service in the Fiber app's state, and an error if the config is nil.\n// The container should be a function like redis.Run or postgres.Run that returns a container type\n// which embeds [tc.Container].\n// - The cfg is the Fiber app's configuration, needed to add the service to the Fiber app's state.\n// - The containerConfig is the configuration for the container, where:\n//   - The containerConfig.ServiceKey is the key used to identify the service in the Fiber app's state.\n//   - The containerConfig.Run is the function to use to run the container. It's usually the Run function from the module, like redis.Run or postgres.Run.\n//   - The containerConfig.Image is the image to use for the container.\n//   - The containerConfig.Options are the functional options to pass to the [tc.Run] function. This argument is optional.\n//\n// Use [NewModuleConfig] or [NewContainerConfig] helper functions to create valid containerConfig objects.\nfunc AddService[T tc.Container](cfg *fiber.Config, containerConfig Config[T]) (*ContainerService[T], error) {\n\tif cfg == nil {\n\t\treturn nil, ErrNilConfig\n\t}\n\n\tif containerConfig.ServiceKey == \"\" {\n\t\treturn nil, ErrEmptyServiceKey\n\t}\n\n\tif containerConfig.Image == \"\" {\n\t\treturn nil, ErrImageEmpty\n\t}\n\n\tif containerConfig.Run == nil {\n\t\treturn nil, ErrRunNil\n\t}\n\n\tk := buildKey(containerConfig.ServiceKey)\n\n\tc := &ContainerService[T]{\n\t\tkey:  k,\n\t\timg:  containerConfig.Image,\n\t\topts: containerConfig.Options,\n\t\trun:  containerConfig.Run,\n\t}\n\n\tcfg.Services = append(cfg.Services, c)\n\n\treturn c, nil\n}\n"
  },
  {
    "path": "v3/testcontainers/testcontainers_test.go",
    "content": "package testcontainers_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gofiber/contrib/v3/testcontainers\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\ttc \"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/modules/redis\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n)\n\nconst (\n\tnginxAlpineImg    = \"nginx:alpine\"\n\tredisAlpineImg    = \"redis:alpine\"\n\tpostgresAlpineImg = \"postgres:alpine\"\n)\n\nfunc TestAddService_fromContainerConfig(t *testing.T) {\n\tt.Run(\"nil-config\", func(t *testing.T) {\n\t\tcontainerConfig := testcontainers.NewContainerConfig(\"nginx-generic\", nginxAlpineImg)\n\n\t\tsrv, err := testcontainers.AddService(nil, containerConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrNilConfig)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"empty-service-key\", func(t *testing.T) {\n\t\tcontainerConfig := testcontainers.NewContainerConfig(\"\", nginxAlpineImg)\n\n\t\tsrv, err := testcontainers.AddService(&fiber.Config{}, containerConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrEmptyServiceKey)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"empty-image\", func(t *testing.T) {\n\t\tcontainerConfig := testcontainers.NewContainerConfig(\"nginx-generic\", \"\")\n\n\t\tsrv, err := testcontainers.AddService(&fiber.Config{}, containerConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrImageEmpty)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tcfg := fiber.Config{}\n\n\t\tcontainerConfig := testcontainers.NewContainerConfig(\"nginx-generic\", nginxAlpineImg, tc.WithExposedPorts(\"80/tcp\"))\n\n\t\tsrv, err := testcontainers.AddService(&cfg, containerConfig)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"nginx-generic (using testcontainers-go)\", srv.Key())\n\n\t\tapp := fiber.New(cfg)\n\n\t\trequire.Len(t, app.State().Services(), 1)\n\t\trequire.Equal(t, 1, app.State().ServicesLen())\n\t})\n}\n\nfunc TestAddService_fromModuleConfig(t *testing.T) {\n\tt.Run(\"nil-fiber-config\", func(t *testing.T) {\n\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module\", redisAlpineImg, redis.Run)\n\n\t\tsrv, err := testcontainers.AddService(nil, moduleConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrNilConfig)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"empty-service-key\", func(t *testing.T) {\n\t\tmoduleConfig := testcontainers.NewModuleConfig(\"\", redisAlpineImg, redis.Run)\n\n\t\tsrv, err := testcontainers.AddService(&fiber.Config{}, moduleConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrEmptyServiceKey)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"empty-image\", func(t *testing.T) {\n\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module\", \"\", redis.Run)\n\n\t\tsrv, err := testcontainers.AddService(&fiber.Config{}, moduleConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrImageEmpty)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"nil-run-fn\", func(t *testing.T) {\n\t\tvar run func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (tc.Container, error)\n\n\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module\", redisAlpineImg, run)\n\n\t\tsrv, err := testcontainers.AddService(&fiber.Config{}, moduleConfig)\n\t\trequire.ErrorIs(t, err, testcontainers.ErrRunNil)\n\t\trequire.Nil(t, srv)\n\t})\n\n\tt.Run(\"add-modules\", func(t *testing.T) {\n\t\tcfg := fiber.Config{}\n\n\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module\", redisAlpineImg, redis.Run)\n\n\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"redis-module (using testcontainers-go)\", srv.Key())\n\n\t\tapp := fiber.New(cfg)\n\n\t\trequire.Len(t, app.State().Services(), 1)\n\t\trequire.Equal(t, 1, app.State().ServicesLen())\n\t})\n}\n\nfunc TestContainerService(t *testing.T) {\n\tt.Run(\"start\", func(t *testing.T) {\n\t\tcfg := fiber.Config{}\n\n\t\tt.Run(\"success\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module\", redisAlpineImg, redis.Run)\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.NoError(t, srv.Start(context.Background()))\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, srv.Terminate(context.Background()))\n\t\t\t})\n\n\t\t\tctr := srv.Container()\n\t\t\trequire.NotNil(t, ctr)\n\n\t\t\tst, err := srv.State(context.Background())\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, \"running\", st)\n\n\t\t\t// verify the container has the correct labels\n\t\t\tinspect, err := srv.Container().Inspect(context.Background())\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, \"Go Fiber\", inspect.Config.Labels[\"org.testcontainers.golang.framework\"])\n\t\t})\n\n\t\tt.Run(\"error\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module-error\", redisAlpineImg, redis.Run, tc.WithWaitStrategy(wait.ForLog(\"never happens\").WithStartupTimeout(time.Second)))\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Error(t, srv.Start(context.Background()))\n\n\t\t\tctr := srv.Container()\n\t\t\trequire.Nil(t, ctr)\n\t\t})\n\n\t\tt.Run(\"twice-error\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module-twice-error\", redisAlpineImg, redis.Run)\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.NoError(t, srv.Start(context.Background()))\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, srv.Terminate(context.Background()))\n\t\t\t})\n\n\t\t\trequire.Error(t, srv.Start(context.Background()))\n\t\t})\n\t})\n\n\tt.Run(\"state\", func(t *testing.T) {\n\t\tcfg := fiber.Config{}\n\n\t\tt.Run(\"running\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module-running\", redisAlpineImg, redis.Run)\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"redis-module-running (using testcontainers-go)\", srv.String())\n\n\t\t\trequire.NoError(t, srv.Start(context.Background()))\n\t\t\tt.Cleanup(func() {\n\t\t\t\trequire.NoError(t, srv.Terminate(context.Background()))\n\t\t\t})\n\n\t\t\tst, err := srv.State(context.Background())\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"running\", st)\n\t\t})\n\n\t\tt.Run(\"not-running\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module-not-running\", redisAlpineImg, redis.Run)\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"redis-module-not-running (using testcontainers-go)\", srv.String())\n\n\t\t\tst, err := srv.State(context.Background())\n\t\t\trequire.ErrorIs(t, err, testcontainers.ErrContainerNotRunning)\n\t\t\trequire.Empty(t, st)\n\t\t})\n\t})\n\n\tt.Run(\"terminate\", func(t *testing.T) {\n\t\tcfg := fiber.Config{}\n\n\t\tt.Run(\"running\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module\", redisAlpineImg, redis.Run)\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Start the service to be able to terminate it.\n\t\t\trequire.NoError(t, srv.Start(context.Background()))\n\n\t\t\trequire.NoError(t, srv.Terminate(context.Background()))\n\n\t\t\t// The container is terminated, so the state should not be available.\n\t\t\t_, err = srv.State(context.Background())\n\t\t\trequire.Error(t, err)\n\t\t})\n\n\t\tt.Run(\"not-running\", func(t *testing.T) {\n\t\t\tmoduleConfig := testcontainers.NewModuleConfig(\"redis-module-not-running\", redisAlpineImg, redis.Run)\n\n\t\t\tsrv, err := testcontainers.AddService(&cfg, moduleConfig)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, \"redis-module-not-running (using testcontainers-go)\", srv.String())\n\n\t\t\terr = srv.Terminate(context.Background())\n\t\t\trequire.ErrorIs(t, err, testcontainers.ErrContainerNotRunning)\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "v3/testcontainers/testcontainers_unit_test.go",
    "content": "package testcontainers\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/testcontainers/testcontainers-go/modules/redis\"\n)\n\nfunc Test_buildKey(t *testing.T) {\n\tt.Run(\"no-suffix\", func(t *testing.T) {\n\t\tkey := \"test\"\n\t\tgot := buildKey(key)\n\t\trequire.Equal(t, key+serviceSuffix, got)\n\t})\n\n\tt.Run(\"with-suffix\", func(t *testing.T) {\n\t\tkey := \"test-suffix\" + serviceSuffix\n\t\tgot := buildKey(key)\n\t\trequire.Equal(t, key, got)\n\t})\n}\n\nfunc Test_ContainersService_Start(t *testing.T) {\n\tt.Run(\"twice-error\", func(t *testing.T) {\n\t\tcfg := fiber.Config{}\n\n\t\tmoduleConfig := NewModuleConfig(\"redis-module-twice-error\", \"redis:alpine\", redis.Run)\n\n\t\tsrv, err := AddService(&cfg, moduleConfig)\n\t\trequire.NoError(t, err)\n\n\t\topts1 := srv.opts\n\n\t\trequire.NoError(t, srv.Start(context.Background()))\n\t\tt.Cleanup(func() {\n\t\t\trequire.NoError(t, srv.Terminate(context.Background()))\n\t\t})\n\n\t\trequire.Error(t, srv.Start(context.Background()))\n\n\t\t// verify that the opts are not modified\n\t\topts2 := srv.opts\n\t\trequire.Equal(t, opts1, opts2)\n\t})\n}\n"
  },
  {
    "path": "v3/websocket/README.md",
    "content": "---\nid: websocket\n---\n\n# Websocket\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*websocket*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20websocket/badge.svg)\n\nBased on [Fasthttp WebSocket](https://github.com/fasthttp/websocket) for [Fiber](https://github.com/gofiber/fiber) with available `fiber.Ctx` methods like [Locals](http://docs.gofiber.io/ctx#locals), [Params](http://docs.gofiber.io/ctx#params), [Query](http://docs.gofiber.io/ctx#query) and [Cookies](http://docs.gofiber.io/ctx#cookies).\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/websocket\n```\n\n## Signatures\n```go\nfunc New(handler func(*websocket.Conn), config ...websocket.Config) fiber.Handler {\n```\n\n## Config\n\n| Property            | Type                         | Description                                                                                                                   | Default                |\n|:--------------------|:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------|:-----------------------|\n| Next                | `func(fiber.Ctx) bool`       | Defines a function to skip this middleware when it returns true.                                                              | `nil`                  |\n| HandshakeTimeout    | `time.Duration`              | HandshakeTimeout specifies the duration for the handshake to complete.                                                        | `0` (No timeout)       |\n| Subprotocols        | `[]string`                   | Subprotocols specifies the client's requested subprotocols.                                                                   | `nil`                  |\n| Origins             | `[]string`                   | Allowed Origins based on the Origin header. If empty, everything is allowed.                                                  | `nil`                  |\n| AllowEmptyOrigin    | `bool`                       | Allows connections without an Origin header when Origins is configured. Useful for non-browser clients.                       | `false`                |\n| ReadBufferSize      | `int`                        | ReadBufferSize specifies the I/O buffer size in bytes for incoming messages.                                                  | `0` (Use default size) |\n| WriteBufferSize     | `int`                        | WriteBufferSize specifies the I/O buffer size in bytes for outgoing messages.                                                 | `0` (Use default size) |\n| WriteBufferPool     | `websocket.BufferPool`       | WriteBufferPool is a pool of buffers for write operations.                                                                    | `nil`                  |\n| EnableCompression   | `bool`                       | EnableCompression specifies if the client should attempt to negotiate per message compression (RFC 7692).                     | `false`                |\n| RecoverHandler      | `func(*websocket.Conn)`      | RecoverHandler is a panic handler function that recovers from panics.                                                         | `defaultRecover`       |\n\n## Example\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/contrib/v3/websocket\"\n)\n\nfunc main() {\n    app := fiber.New()\n\n    app.Use(\"/ws\", func(c fiber.Ctx) error {\n        // IsWebSocketUpgrade returns true if the client\n        // requested upgrade to the WebSocket protocol.\n        if websocket.IsWebSocketUpgrade(c) {\n            c.Locals(\"allowed\", true)\n            return c.Next()\n        }\n        return fiber.ErrUpgradeRequired\n    })\n\n    app.Get(\"/ws/:id\", websocket.New(func(c *websocket.Conn) {\n        // c.Locals is added to the *websocket.Conn\n        log.Println(c.Locals(\"allowed\"))  // true\n        log.Println(c.Params(\"id\"))       // 123\n        log.Println(c.Query(\"v\"))         // 1.0\n        log.Println(c.Cookies(\"session\")) // \"\"\n\n        // websocket.Conn bindings https://pkg.go.dev/github.com/fasthttp/websocket?tab=doc#pkg-index\n        var (\n            mt  int\n            msg []byte\n            err error\n        )\n        for {\n            if mt, msg, err = c.ReadMessage(); err != nil {\n                log.Println(\"read:\", err)\n                break\n            }\n            log.Printf(\"recv: %s\", msg)\n\n            if err = c.WriteMessage(mt, msg); err != nil {\n                log.Println(\"write:\", err)\n                break\n            }\n        }\n\n    }))\n\n    log.Fatal(app.Listen(\":3000\"))\n    // Access the websocket server: ws://localhost:3000/ws/123?v=1.0\n    // https://www.websocket.org/echo.html\n}\n\n```\n\n## Note with cache middleware\n\nIf you get the error `websocket: bad handshake` when using the [cache middleware](https://github.com/gofiber/fiber/tree/master/middleware/cache), please use `config.Next` to skip websocket path.\n\n```go\napp := fiber.New()\napp.Use(cache.New(cache.Config{\n        Next: func(c fiber.Ctx) bool {\n            return strings.Contains(c.Route().Path, \"/ws\")\n        },\n}))\n\napp.Get(\"/ws/:id\", websocket.New(func(c *websocket.Conn) {}))\n```\n\n## Note with recover middleware\n\nFor internal implementation reasons, currently recover middleware does not work with websocket middleware, please use `config.RecoverHandler` to add recover handler to websocket endpoints.\nBy default, config `RecoverHandler` recovers from panic and writes stack trace to stderr, also returns a response that contains panic message in **error** field.\n\n```go\napp := fiber.New()\n\napp.Use(cache.New(cache.Config{\n    Next: func(c fiber.Ctx) bool {\n        return strings.Contains(c.Route().Path, \"/ws\")\n    },\n}))\n\ncfg := Config{\n    RecoverHandler: func(conn *Conn) {\n        if err := recover(); err != nil {\n            conn.WriteJSON(fiber.Map{\"customError\": \"error occurred\"})\n        }\n    },\n}\n\napp.Get(\"/ws/:id\", websocket.New(func(c *websocket.Conn) {}, cfg))\n\n```\n\n## Note for WebSocket subprotocols\n\nThe config `Subprotocols` only helps you negotiate subprotocols and sets a `Sec-Websocket-Protocol` header if it has a suitable subprotocol. For more about negotiates process, check the comment for `Subprotocols` in [fasthttp.Upgrader](https://pkg.go.dev/github.com/fasthttp/websocket#Upgrader) .\n\nAll connections will be sent to the handler function no matter whether the subprotocol negotiation is successful or not. You can get the selected subprotocol from `conn.Subprotocol()`. \n\nIf a connection includes the `Sec-Websocket-Protocol` header in the request but the protocol negotiation fails, the browser will immediately disconnect the connection after receiving the upgrade response.\n"
  },
  {
    "path": "v3/websocket/go.mod",
    "content": "module github.com/gofiber/contrib/v3/websocket\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/fasthttp/websocket v1.5.12\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/valyala/fasthttp v1.70.0\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/websocket/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=\ngithub.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=\ngithub.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/websocket/websocket.go",
    "content": "// 🚀 Fiber is an Express inspired web framework written in Go with 💖\n// 📌 API Documentation: https://fiber.wiki\n// 📝 Github Repository: https://github.com/gofiber/fiber\n\npackage websocket\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/fasthttp/websocket\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\t\"github.com/valyala/fasthttp\"\n)\n\n// Config ...\ntype Config struct {\n\t// Next defines a function to skip this middleware when it returns true.\n\t// Optional. Default: nil\n\tNext func(fiber.Ctx) bool\n\n\t// HandshakeTimeout specifies the duration for the handshake to complete.\n\tHandshakeTimeout time.Duration\n\n\t// Subprotocols specifies the client's requested subprotocols.\n\tSubprotocols []string\n\n\t// Allowed Origin's based on the Origin header, this validate the request origin to\n\t// prevent cross-site request forgery. Everything is allowed if left empty.\n\tOrigins []string\n\n\t// AllowEmptyOrigin allows WebSocket connections when the Origin header is absent.\n\t// When false (default), connections without an Origin header are rejected unless Origins includes \"*\".\n\t// Set to true to allow connections from non-browser clients that don't send Origin headers.\n\t// Optional. Default: false\n\tAllowEmptyOrigin bool\n\n\t// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer\n\t// size is zero, then a useful default size is used. The I/O buffer sizes\n\t// do not limit the size of the messages that can be sent or received.\n\tReadBufferSize, WriteBufferSize int\n\n\t// WriteBufferPool is a pool of buffers for write operations. If the value\n\t// is not set, then write buffers are allocated to the connection for the\n\t// lifetime of the connection.\n\t//\n\t// A pool is most useful when the application has a modest volume of writes\n\t// across a large number of connections.\n\t//\n\t// Applications should use a single pool for each unique value of\n\t// WriteBufferSize.\n\tWriteBufferPool websocket.BufferPool\n\n\t// EnableCompression specifies if the client should attempt to negotiate\n\t// per message compression (RFC 7692). Setting this value to true does not\n\t// guarantee that compression will be supported. Currently only \"no context\n\t// takeover\" modes are supported.\n\tEnableCompression bool\n\n\t// RecoverHandler is a panic handler function that recovers from panics\n\t// Default recover function is used when nil and writes error message in a response field `error`\n\t// It prints stack trace to the stderr by default\n\t// Optional. Default: defaultRecover\n\tRecoverHandler func(*Conn)\n}\n\nfunc defaultRecover(c *Conn) {\n\tif err := recover(); err != nil {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"panic: %v\\n%s\\n\", err, debug.Stack()) //nolint:errcheck // This will never fail\n\t\tif err := c.WriteJSON(fiber.Map{\"error\": err}); err != nil {\n\t\t\t_, _ = fmt.Fprintf(os.Stderr, \"could not write error response: %v\\n\", err)\n\t\t}\n\t}\n}\n\n// New returns a new `handler func(*Conn)` that upgrades a client to the\n// websocket protocol, you can pass an optional config.\nfunc New(handler func(*Conn), config ...Config) fiber.Handler {\n\t// Init config\n\tvar cfg Config\n\tif len(config) > 0 {\n\t\tcfg = config[0]\n\t}\n\tif len(cfg.Origins) == 0 {\n\t\tcfg.Origins = []string{\"*\"}\n\t}\n\t// Check if wildcard is present in the Origins list during initialization\n\thasWildcard := false\n\tfor _, origin := range cfg.Origins {\n\t\tif origin == \"*\" {\n\t\t\thasWildcard = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif cfg.ReadBufferSize == 0 {\n\t\tcfg.ReadBufferSize = 1024\n\t}\n\tif cfg.WriteBufferSize == 0 {\n\t\tcfg.WriteBufferSize = 1024\n\t}\n\tif cfg.RecoverHandler == nil {\n\t\tcfg.RecoverHandler = defaultRecover\n\t}\n\tvar upgrader = websocket.FastHTTPUpgrader{\n\t\tHandshakeTimeout:  cfg.HandshakeTimeout,\n\t\tSubprotocols:      cfg.Subprotocols,\n\t\tReadBufferSize:    cfg.ReadBufferSize,\n\t\tWriteBufferSize:   cfg.WriteBufferSize,\n\t\tEnableCompression: cfg.EnableCompression,\n\t\tWriteBufferPool:   cfg.WriteBufferPool,\n\t\tCheckOrigin: func(fctx *fasthttp.RequestCtx) bool {\n\t\t\t// Fast path: if Origins is just wildcard (the default), allow all without checking header\n\t\t\tif len(cfg.Origins) == 1 && cfg.Origins[0] == \"*\" {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\torigin := utils.UnsafeString(fctx.Request.Header.Peek(\"Origin\"))\n\t\t\tif origin == \"\" {\n\t\t\t\t// Allow empty Origin if wildcard is in list or explicitly configured\n\t\t\t\treturn hasWildcard || cfg.AllowEmptyOrigin\n\t\t\t}\n\t\t\t// If wildcard is present, allow any non-empty origin\n\t\t\tif hasWildcard {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// No wildcard present, check if origin matches any specific origin in the list\n\t\t\tfor i := range cfg.Origins {\n\t\t\t\tif cfg.Origins[i] == origin {\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\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\t\tif !c.App().Server().KeepHijackedConns {\n\t\t\tc.App().Server().KeepHijackedConns = true\n\t\t}\n\n\t\tconn := acquireConn()\n\t\t// locals\n\t\tc.RequestCtx().VisitUserValues(func(key []byte, value interface{}) {\n\t\t\tconn.locals[string(key)] = value\n\t\t})\n\n\t\t// params\n\t\tparams := c.Route().Params\n\t\tfor i := 0; i < len(params); i++ {\n\t\t\tconn.params[utils.CopyString(params[i])] = utils.CopyString(c.Params(params[i]))\n\t\t}\n\n\t\t// queries\n\t\tqueries := c.RequestCtx().QueryArgs().All()\n\t\tfor key, value := range queries {\n\t\t\tconn.queries[string(key)] = string(value)\n\t\t}\n\n\t\t// cookies\n\t\tcookies := c.RequestCtx().Request.Header.Cookies()\n\t\tfor key, value := range cookies {\n\t\t\tconn.cookies[string(key)] = string(value)\n\t\t}\n\n\t\t// headers\n\t\theaders := c.RequestCtx().Request.Header.All()\n\t\tfor key, value := range headers {\n\t\t\tconn.headers[string(key)] = string(value)\n\t\t}\n\n\t\t// ip address\n\t\tconn.ip = utils.CopyString(c.IP())\n\n\t\tif err := upgrader.Upgrade(c.RequestCtx(), func(fconn *websocket.Conn) {\n\t\t\tconn.Conn = fconn\n\t\t\tdefer releaseConn(conn)\n\t\t\tdefer cfg.RecoverHandler(conn)\n\t\t\thandler(conn)\n\t\t}); err != nil { // Upgrading required\n\t\t\treleaseConn(conn)\n\t\t\treturn fiber.ErrUpgradeRequired\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// Conn https://godoc.org/github.com/gorilla/websocket#pkg-index\ntype Conn struct {\n\t*websocket.Conn\n\tlocals  map[string]interface{}\n\tparams  map[string]string\n\tcookies map[string]string\n\theaders map[string]string\n\tqueries map[string]string\n\tip      string\n}\n\n// Conn pool\nvar poolConn = sync.Pool{\n\tNew: func() interface{} {\n\t\treturn new(Conn)\n\t},\n}\n\n// Acquire Conn from pool\nfunc acquireConn() *Conn {\n\tconn := poolConn.Get().(*Conn)\n\tconn.locals = make(map[string]interface{})\n\tconn.params = make(map[string]string)\n\tconn.queries = make(map[string]string)\n\tconn.cookies = make(map[string]string)\n\tconn.headers = make(map[string]string)\n\treturn conn\n}\n\n// Return Conn to pool\nfunc releaseConn(conn *Conn) {\n\tconn.Conn = nil\n\tpoolConn.Put(conn)\n}\n\n// Locals makes it possible to pass interface{} values under string keys scoped to the request\n// and therefore available to all following routes that match the request.\nfunc (conn *Conn) Locals(key string, value ...interface{}) interface{} {\n\tif len(value) == 0 {\n\t\treturn conn.locals[key]\n\t}\n\tconn.locals[key] = value[0]\n\treturn value[0]\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.\nfunc (conn *Conn) Params(key string, defaultValue ...string) string {\n\tv, ok := conn.params[key]\n\tif !ok && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn v\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.\nfunc (conn *Conn) Query(key string, defaultValue ...string) string {\n\tv, ok := conn.queries[key]\n\tif !ok && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn v\n}\n\n// Cookies is used for getting a cookie value by key\n// Defaults to 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.\nfunc (conn *Conn) Cookies(key string, defaultValue ...string) string {\n\tv, ok := conn.cookies[key]\n\tif !ok && len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn v\n}\n\n// Headers is used for getting a header value by key\n// Defaults to empty string \"\" if the header doesn't exist.\n// If a default value is given, it will return that value if the header doesn't exist.\n// Header lookups are case-insensitive.\nfunc (conn *Conn) Headers(key string, defaultValue ...string) string {\n\tfor k, v := range conn.headers {\n\t\tif utils.EqualFold(k, key) {\n\t\t\treturn v\n\t\t}\n\t}\n\tif len(defaultValue) > 0 {\n\t\treturn defaultValue[0]\n\t}\n\treturn \"\"\n}\n\n// IP returns the client's network address\nfunc (conn *Conn) IP() string {\n\treturn conn.ip\n}\n\n// Constants are taken from https://github.com/fasthttp/websocket/blob/master/conn.go#L43\n\n// Close codes defined in RFC 6455, section 11.7.\nconst (\n\tCloseNormalClosure           = 1000\n\tCloseGoingAway               = 1001\n\tCloseProtocolError           = 1002\n\tCloseUnsupportedData         = 1003\n\tCloseNoStatusReceived        = 1005\n\tCloseAbnormalClosure         = 1006\n\tCloseInvalidFramePayloadData = 1007\n\tClosePolicyViolation         = 1008\n\tCloseMessageTooBig           = 1009\n\tCloseMandatoryExtension      = 1010\n\tCloseInternalServerErr       = 1011\n\tCloseServiceRestart          = 1012\n\tCloseTryAgainLater           = 1013\n\tCloseTLSHandshake            = 1015\n)\n\n// The message types are defined in RFC 6455, section 11.8.\nconst (\n\t// TextMessage denotes a text data message. The text message payload is\n\t// interpreted as UTF-8 encoded text data.\n\tTextMessage = 1\n\n\t// BinaryMessage denotes a binary data message.\n\tBinaryMessage = 2\n\n\t// CloseMessage denotes a close control message. The optional message\n\t// payload contains a numeric code and text. Use the FormatCloseMessage\n\t// function to format a close message payload.\n\tCloseMessage = 8\n\n\t// PingMessage denotes a ping control message. The optional message payload\n\t// is UTF-8 encoded text.\n\tPingMessage = 9\n\n\t// PongMessage denotes a pong control message. The optional message payload\n\t// is UTF-8 encoded text.\n\tPongMessage = 10\n)\n\nvar (\n\t// ErrBadHandshake is returned when the server response to opening handshake is\n\t// invalid.\n\tErrBadHandshake = errors.New(\"websocket: bad handshake\")\n\t// ErrCloseSent is returned when the application writes a message to the\n\t// connection after sending a close message.\n\tErrCloseSent = errors.New(\"websocket: close sent\")\n\t// ErrReadLimit is returned when reading a message that is larger than the\n\t// read limit set for the connection.\n\tErrReadLimit = errors.New(\"websocket: read limit exceeded\")\n)\n\n// FormatCloseMessage formats closeCode and text as a WebSocket close message.\n// An empty message is returned for code CloseNoStatusReceived.\nfunc FormatCloseMessage(closeCode int, text string) []byte {\n\treturn websocket.FormatCloseMessage(closeCode, text)\n}\n\n// IsCloseError returns boolean indicating whether the error is a *CloseError\n// with one of the specified codes.\nfunc IsCloseError(err error, codes ...int) bool {\n\treturn websocket.IsCloseError(err, codes...)\n}\n\n// IsUnexpectedCloseError returns boolean indicating whether the error is a\n// *CloseError with a code not in the list of expected codes.\nfunc IsUnexpectedCloseError(err error, expectedCodes ...int) bool {\n\treturn websocket.IsUnexpectedCloseError(err, expectedCodes...)\n}\n\n// IsWebSocketUpgrade returns true if the client requested upgrade to the\n// WebSocket protocol.\nfunc IsWebSocketUpgrade(c fiber.Ctx) bool {\n\treturn websocket.FastHTTPIsWebSocketUpgrade(c.RequestCtx())\n}\n\n// JoinMessages concatenates received messages to create a single io.Reader.\n// The string term is appended to each message. The returned reader does not\n// support concurrent calls to the Read method.\nfunc JoinMessages(c *websocket.Conn, term string) io.Reader {\n\treturn websocket.JoinMessages(c, term)\n}\n"
  },
  {
    "path": "v3/websocket/websocket_test.go",
    "content": "package websocket\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fasthttp/websocket\"\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWebSocketMiddlewareDefaultConfig(t *testing.T) {\n\tapp := setupTestApp(Config{}, nil)\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketMiddlewareConfigOrigin(t *testing.T) {\n\tt.Run(\"allow all origins\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins: []string{\"*\"},\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", http.Header{\n\t\t\t\"Origin\": []string{\"http://localhost:3000\"},\n\t\t})\n\t\tdefer conn.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode)\n\t\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\t\tvar msg fiber.Map\n\t\terr = conn.ReadJSON(&msg)\n\t\tassert.Nil(t, err)\n\t\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n\t})\n\n\tt.Run(\"allowed origin\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins: []string{\"http://localhost:3000\"},\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", http.Header{\n\t\t\t\"Origin\": []string{\"http://localhost:3000\"},\n\t\t})\n\t\tdefer conn.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode)\n\t\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\t\tvar msg fiber.Map\n\t\terr = conn.ReadJSON(&msg)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n\t})\n\n\tt.Run(\"empty origin rejected by default\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins: []string{\"http://localhost:3000\"},\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\t\tif conn != nil {\n\t\t\tdefer conn.Close()\n\t\t}\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"bad handshake\")\n\t\tassert.Equal(t, fiber.StatusUpgradeRequired, resp.StatusCode)\n\t\tassert.Equal(t, \"\", resp.Header.Get(\"Upgrade\"))\n\n\t\tassert.Nil(t, conn)\n\t})\n\n\tt.Run(\"empty origin allowed with config\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins:          []string{\"http://localhost:3000\"},\n\t\t\tAllowEmptyOrigin: true,\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\t\tdefer conn.Close()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode)\n\t\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\t\tvar msg fiber.Map\n\t\terr = conn.ReadJSON(&msg)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n\t})\n\n\tt.Run(\"wildcard in list\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins: []string{\"http://localhost:3000\", \"*\"},\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", http.Header{\n\t\t\t\"Origin\": []string{\"http://localhost:5000\"},\n\t\t})\n\t\tif !assert.NoError(t, err) {\n\t\t\treturn\n\t\t}\n\t\tdefer conn.Close()\n\t\tassert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode)\n\t\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\t\tvar msg fiber.Map\n\t\terr = conn.ReadJSON(&msg)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n\t})\n\n\tt.Run(\"wildcard in list allows empty origin\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins: []string{\"http://localhost:3000\", \"*\"},\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\t\t// Explicitly test with no Origin header (nil headers = no Origin sent)\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\t\tif !assert.NoError(t, err) {\n\t\t\treturn\n\t\t}\n\t\tdefer conn.Close()\n\t\tassert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode)\n\t\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\t\tvar msg fiber.Map\n\t\terr = conn.ReadJSON(&msg)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n\t})\n\n\tt.Run(\"disallowed origin\", func(t *testing.T) {\n\t\tapp := setupTestApp(Config{\n\t\t\tOrigins: []string{\"http://localhost:3000\"},\n\t\t}, nil)\n\t\tdefer app.Shutdown()\n\t\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", http.Header{\n\t\t\t\"Origin\": []string{\"http://localhost:5000\"},\n\t\t})\n\t\tdefer conn.Close()\n\t\tassert.Equal(t, err.Error(), \"websocket: bad handshake\")\n\t\tassert.Equal(t, fiber.StatusUpgradeRequired, resp.StatusCode)\n\t\tassert.Equal(t, \"\", resp.Header.Get(\"Upgrade\"))\n\n\t\tassert.Nil(t, conn)\n\t})\n}\n\nfunc TestWebSocketMiddlewareBufferSize(t *testing.T) {\n\tapp := setupTestApp(Config{\n\t\tOrigins:         []string{\"*\"},\n\t\tWriteBufferSize: 10,\n\t}, nil)\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketConnParams(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\tparam1 := c.Params(\"param1\")\n\t\tparam2 := c.Params(\"param2\")\n\t\tparamDefault := c.Params(\"paramDefault\", \"default\")\n\n\t\tassert.Equal(t, \"value1\", param1)\n\t\tassert.Equal(t, \"value2\", param2)\n\t\tassert.Equal(t, \"default\", paramDefault)\n\n\t\tc.WriteJSON(fiber.Map{\n\t\t\t\"message\": \"hello websocket\",\n\t\t})\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message/value1/value2\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketConnQuery(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\tquery1 := c.Query(\"query1\")\n\t\tquery2 := c.Query(\"query2\")\n\t\tqueryDefault := c.Query(\"queryDefault\", \"default\")\n\n\t\tassert.Equal(t, \"value1\", query1)\n\t\tassert.Equal(t, \"value2\", query2)\n\t\tassert.Equal(t, \"default\", queryDefault)\n\n\t\tc.WriteJSON(fiber.Map{\n\t\t\t\"message\": \"hello websocket\",\n\t\t})\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message?query1=value1&query2=value2\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketConnHeaders(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\theader1 := c.Headers(\"Header1\")\n\t\theader2 := c.Headers(\"Header2\")\n\t\tcontentType := c.Headers(\"Content-Type\")\n\t\theaderDefault := c.Headers(\"HeaderDefault\", \"valueDefault\")\n\n\t\tassert.Equal(t, \"value1\", header1)\n\t\tassert.Equal(t, \"value2\", header2)\n\t\tassert.Equal(t, \"application/json\", contentType)\n\t\tassert.Equal(t, \"valueDefault\", headerDefault)\n\n\t\tc.WriteJSON(fiber.Map{\n\t\t\t\"message\": \"hello websocket\",\n\t\t})\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", http.Header{\n\t\t\"header1\":      []string{\"value1\"},\n\t\t\"header2\":      []string{\"value2\"},\n\t\t\"content-type\": []string{\"application/json\"},\n\t})\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketConnCookies(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\tcookie1 := c.Cookies(\"Cookie1\")\n\t\tcookie2 := c.Cookies(\"Cookie2\")\n\t\tcookieDefault := c.Headers(\"CookieDefault\", \"valueDefault\")\n\n\t\tassert.Equal(t, \"value1\", cookie1)\n\t\tassert.Equal(t, \"value2\", cookie2)\n\t\tassert.Equal(t, \"valueDefault\", cookieDefault)\n\n\t\tc.WriteJSON(fiber.Map{\n\t\t\t\"message\": \"hello websocket\",\n\t\t})\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", http.Header{\n\t\t\"header1\": []string{\"value1\"},\n\t\t\"header2\": []string{\"value2\"},\n\t\t\"Cookie\":  []string{\"Cookie1=value1; Cookie2=value2\"},\n\t})\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketConnLocals(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\tlocal1 := c.Locals(\"local1\")\n\t\tlocal2 := c.Locals(\"local2\")\n\n\t\tassert.Equal(t, \"value1\", local1)\n\t\tassert.Equal(t, \"value2\", local2)\n\n\t\tc.WriteJSON(fiber.Map{\n\t\t\t\"message\": \"hello websocket\",\n\t\t})\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\nfunc TestWebSocketConnIP(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\tip := c.IP()\n\n\t\tassert.Equal(t, \"127.0.0.1\", ip)\n\n\t\tc.WriteJSON(fiber.Map{\n\t\t\t\"message\": \"hello websocket\",\n\t\t})\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n}\n\n// TestWebSocketConnIPSafeCopy verifies that conn.IP() returns a safe copy\n// that is not corrupted when fasthttp reuses its internal buffer for\n// subsequent requests. See: gofiber/fiber#4208, gofiber/contrib#1800\nfunc TestWebSocketConnIPSafeCopy(t *testing.T) {\n\tconst iterations = 5\n\tips := make(chan string, iterations)\n\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\t// Read the IP and send it back; the value must remain \"127.0.0.1\"\n\t\t// even after fasthttp recycles the underlying request buffer.\n\t\tips <- c.IP()\n\t\tc.WriteJSON(fiber.Map{\"ip\": c.IP()})\n\t})\n\tdefer app.Shutdown()\n\n\tfor i := 0; i < iterations; i++ {\n\t\tconn, _, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\t\trequire.NoError(t, err)\n\t\tvar msg fiber.Map\n\t\terr = conn.ReadJSON(&msg)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"127.0.0.1\", msg[\"ip\"])\n\t\tconn.Close()\n\t}\n\n\tclose(ips)\n\tfor ip := range ips {\n\t\tassert.Equal(t, \"127.0.0.1\", ip, \"conn.IP() must be a safe copy, not a reference to recycled fasthttp buffer\")\n\t}\n}\n\nfunc TestWebSocketCompressionAfterHandlerReturns(t *testing.T) {\n\twriteErr := make(chan error, 1)\n\thandlerReturning := make(chan struct{})\n\tapp := setupTestApp(Config{\n\t\tEnableCompression: true,\n\t}, func(c *Conn) {\n\t\tdefer close(handlerReturning)\n\t\tconn := c.Conn\n\t\tgo func() {\n\t\t\t<-handlerReturning\n\t\t\tconn.EnableWriteCompression(true)\n\t\t\tif err := conn.SetCompressionLevel(2); err != nil {\n\t\t\t\twriteErr <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\twriteErr <- conn.WriteJSON(fiber.Map{\"message\": \"hello websocket\"})\n\t\t}()\n\t})\n\tdefer app.Shutdown()\n\n\tdialer := websocket.Dialer{\n\t\tEnableCompression: true,\n\t}\n\tconn, resp, err := dialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\trequire.NoError(t, err)\n\tdefer conn.Close()\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\tassert.Contains(t, resp.Header.Get(\"Sec-WebSocket-Extensions\"), \"permessage-deflate\")\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"hello websocket\", msg[\"message\"])\n\n\tselect {\n\tcase err := <-writeErr:\n\t\tassert.NoError(t, err)\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"timed out waiting for async compressed write\")\n\t}\n}\n\nfunc setupTestApp(cfg Config, h func(c *Conn)) *fiber.App {\n\tvar handler fiber.Handler\n\tif h == nil {\n\t\thandler = New(func(c *Conn) {\n\t\t\tc.WriteJSON(fiber.Map{\n\t\t\t\t\"message\": \"hello websocket\",\n\t\t\t})\n\t\t}, cfg)\n\t} else {\n\t\thandler = New(h, cfg)\n\t}\n\n\tapp := fiber.New(fiber.Config{})\n\n\tapp.Use(\"/ws\", func(c fiber.Ctx) error {\n\t\tif IsWebSocketUpgrade(c) {\n\t\t\tfiber.StoreInContext(c, \"allowed\", true)\n\t\t\tfiber.StoreInContext(c, \"local1\", \"value1\")\n\t\t\tfiber.StoreInContext(c, \"local2\", \"value2\")\n\t\t\treturn c.Next()\n\t\t}\n\t\treturn fiber.ErrUpgradeRequired\n\t})\n\n\tapp.Get(\"/ws/message\", handler)\n\tapp.Get(\"/ws/message/:param1/:param2\", handler)\n\tgo app.Listen(\":3000\", fiber.ListenConfig{DisableStartupMessage: true})\n\n\treadyCh := make(chan struct{})\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := net.Dial(\"tcp\", \"localhost:3000\")\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif conn != nil {\n\t\t\t\treadyCh <- struct{}{}\n\t\t\t\tconn.Close()\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-readyCh\n\n\treturn app\n}\n\nfunc TestWebSocketIsCloseError(t *testing.T) {\n\tcloseError := IsCloseError(&websocket.CloseError{\n\t\tCode: websocket.CloseNormalClosure,\n\t}, websocket.CloseNormalClosure)\n\tassert.Equal(t, true, closeError)\n}\n\nfunc TestWebSocketIsUnexpectedCloseError(t *testing.T) {\n\tcloseError := IsUnexpectedCloseError(&websocket.CloseError{\n\t\tCode: websocket.CloseNormalClosure,\n\t}, websocket.CloseAbnormalClosure)\n\tassert.Equal(t, true, closeError)\n}\n\nfunc TestWebSocketFormatCloseMessage(t *testing.T) {\n\tcloseMsg := FormatCloseMessage(websocket.CloseNormalClosure, \"test\")\n\n\tassert.Equal(t, []byte{0x3, 0xe8, 0x74, 0x65, 0x73, 0x74}, closeMsg)\n}\n\nfunc TestWebsocketRecoverDefaultHandlerShouldNotPanic(t *testing.T) {\n\tapp := setupTestApp(Config{}, func(c *Conn) {\n\t\tpanic(\"test panic\")\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"test panic\", msg[\"error\"])\n}\n\nfunc TestWebsocketRecoverCustomHandlerShouldNotPanic(t *testing.T) {\n\tapp := setupTestApp(Config{\n\t\tRecoverHandler: func(conn *Conn) {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tconn.WriteJSON(fiber.Map{\"customError\": \"error occurred\"})\n\t\t\t}\n\t\t},\n\t}, func(c *Conn) {\n\t\tpanic(\"test panic\")\n\t})\n\tdefer app.Shutdown()\n\n\tconn, resp, err := websocket.DefaultDialer.Dial(\"ws://localhost:3000/ws/message\", nil)\n\tdefer conn.Close()\n\tassert.NoError(t, err)\n\tassert.Equal(t, 101, resp.StatusCode)\n\tassert.Equal(t, \"websocket\", resp.Header.Get(\"Upgrade\"))\n\n\tvar msg fiber.Map\n\terr = conn.ReadJSON(&msg)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"error occurred\", msg[\"customError\"])\n}\n"
  },
  {
    "path": "v3/zap/.gitignore",
    "content": "all/\ndebug/\ninfo/\nwarn/\nerror/\n*.log\n"
  },
  {
    "path": "v3/zap/README.md",
    "content": "---\nid: zap\n---\n\n# Zap\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*zap*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20zap/badge.svg)\n\n[Zap](https://github.com/uber-go/zap) logging support for Fiber.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/zap\ngo get -u go.uber.org/zap\n```\n\n### Signature\n\n```go\nzap.New(config ...zap.Config) fiber.Handler\n```\n\n### Config\n\n| Property   | Type                       | Description                                                                                                                                                                    | Default                                                                     |\n| :--------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- |\n| Next       | `func(fiber.Ctx) bool`          | Define a function to skip this middleware when returned true                                                                                                                   | `nil`                                                                       |\n| Logger     | `*zap.Logger`              | Add custom zap logger.                                                                                                                                                         | `zap.NewProduction()`                                                      |\n| Fields     | `[]string`                 | Add fields that you want to see.                                                                                                                                                  | `[]string{\"latency\", \"status\", \"method\", \"url\"}`                            |\n| FieldsFunc | `func(fiber.Ctx) []zap.Field` | Define a function to add custom fields.                                                                                                                                        | `nil`                                                                       |\n| Messages   | `[]string`                 | Custom response messages.                                                                                                                                                      | `[]string{\"Server error\", \"Client error\", \"Success\"}`                       |\n| Levels     | `[]zapcore.Level`          | Custom response levels.                                                                                                                                                        | `[]zapcore.Level{zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel}` |\n| SkipURIs   | `[]string`                 | Skip logging these URI.                                                                                                                                                        | `[]string{}`                                                                |\n| GetResBody | `func(c fiber.Ctx) []byte` | Define a function to get response body when return non-nil.<br />eg: When use compress middleware, resBody is unreadable. you can set GetResBody func to get readable resBody. | `nil`                                                                       |\n\n### Example\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n    middleware \"github.com/gofiber/contrib/v3/zap\"\n    \"github.com/gofiber/fiber/v3\"\n    \"go.uber.org/zap\"\n)\n\nfunc main() {\n    app := fiber.New()\n    logger, _ := zap.NewProduction()\n    defer logger.Sync()\n\n    app.Use(middleware.New(middleware.Config{\n        Logger: logger,\n    }))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n\n## NewLogger\n\n### Signature\n\n```go\nzap.NewLogger(config ...zap.LoggerConfig) *zap.LoggerConfig\n```\n\n### LoggerConfig\n\n| Property    | Type           | Description                                                                                              | Default                        |\n| :---------- | :------------- | :------------------------------------------------------------------------------------------------------- | :----------------------------- |\n| CoreConfigs | `[]CoreConfig` | Define Config for zapcore                                                                                | `zap.LoggerConfigDefault` |\n| SetLogger   | `*zap.Logger`  | Add custom zap logger. if not nil, `ZapOptions`, `CoreConfigs`, `SetLevel`, `SetOutput` will be ignored. | `nil`                          |\n| ExtraKeys   | `[]string`     | Allow users log extra values from context.                                                               | `[]string{}`                   |\n| ZapOptions  | `[]zap.Option` | Allow users to configure the zap.Option supplied by zap.                                                 | `[]zap.Option{}`               |\n\n### Example\n\n```go\npackage main\n\nimport (\n    \"context\"\n\n    middleware \"github.com/gofiber/contrib/v3/zap\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/gofiber/fiber/v3/log\"\n)\n\nfunc main() {\n    app := fiber.New()\n    logger := middleware.NewLogger(middleware.LoggerConfig{\n        ExtraKeys: []string{\"request_id\"},\n    })\n    log.SetLogger(logger)\n    defer logger.Sync()\n\n    app.Use(func(c fiber.Ctx) error {\n        ctx := context.WithValue(c.Context(), \"request_id\", \"123\")\n        c.SetContext(ctx)\n        return c.Next()\n    })\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        log.WithContext(c.Context()).Info(\"Hello, World!\")\n        return c.SendString(\"Hello, World!\")\n    })\n    log.Fatal(app.Listen(\":3000\"))\n}\n```\n"
  },
  {
    "path": "v3/zap/config.go",
    "content": "package zap\n\nimport (\n\t\"github.com/gofiber/fiber/v3\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\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// SkipBody defines a function to skip log  \"body\" field when returned true.\n\t//\n\t// Optional. Default: nil\n\tSkipBody func(c fiber.Ctx) bool\n\n\t// SkipResBody defines a function to skip log  \"resBody\" field when returned true.\n\t//\n\t// Optional. Default: nil\n\tSkipResBody func(c fiber.Ctx) bool\n\n\t// GetResBody defines a function to get ResBody.\n\t//  eg: when use compress middleware, resBody is unreadable. you can set GetResBody func to get readable resBody.\n\t//\n\t// Optional. Default: nil\n\tGetResBody func(c fiber.Ctx) []byte\n\n\t// Skip logging for these uri\n\t//\n\t// Optional. Default: nil\n\tSkipURIs []string\n\n\t// Add custom zap logger.\n\t//\n\t// Optional. Default: zap.NewProduction()\n\tLogger *zap.Logger\n\n\t// Add fields what you want see.\n\t//\n\t// Optional. Default: {\"ip\", \"latency\", \"status\", \"method\", \"url\"}\n\tFields []string\n\n\t// FieldsFunc defines a function to return custom zap fields to append to the log.\n\t//\n\t// Optional. Default: nil\n\tFieldsFunc func(c fiber.Ctx) []zap.Field\n\n\t// Custom response messages.\n\t// Response codes >= 500 will be logged with Messages[0].\n\t// Response codes >= 400 will be logged with Messages[1].\n\t// Other response codes will be logged with Messages[2].\n\t// You can specify less, than 3 messages, but you must specify at least 1.\n\t// Specifying more than 3 messages is useless.\n\t//\n\t// Optional. Default: {\"Server error\", \"Client error\", \"Success\"}\n\tMessages []string\n\n\t// Custom response levels.\n\t// Response codes >= 500 will be logged with Levels[0].\n\t// Response codes >= 400 will be logged with Levels[1].\n\t// Other response codes will be logged with Levels[2].\n\t// You can specify less, than 3 levels, but you must specify at least 1.\n\t// Specifying more than 3 levels is useless.\n\t//\n\t// Optional. Default: {zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel}\n\tLevels []zapcore.Level\n}\n\n// Use zap.NewProduction() as default logging instance.\nvar logger, _ = zap.NewProduction()\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:       nil,\n\tLogger:     logger,\n\tFields:     []string{\"ip\", \"latency\", \"status\", \"method\", \"url\"},\n\tFieldsFunc: nil,\n\tMessages:   []string{\"Server error\", \"Client error\", \"Success\"},\n\tLevels:     []zapcore.Level{zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel},\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.Logger == nil {\n\t\tcfg.Logger = ConfigDefault.Logger\n\t}\n\n\tif cfg.Fields == nil {\n\t\tcfg.Fields = ConfigDefault.Fields\n\t}\n\n\tif cfg.Messages == nil {\n\t\tcfg.Messages = ConfigDefault.Messages\n\t}\n\n\tif cfg.Levels == nil {\n\t\tcfg.Levels = ConfigDefault.Levels\n\t}\n\n\tif cfg.FieldsFunc == nil {\n\t\tcfg.FieldsFunc = ConfigDefault.FieldsFunc\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/zap/go.mod",
    "content": "module github.com/gofiber/contrib/v3/zap\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/gofiber/utils/v2 v2.0.3\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/valyala/fasthttp v1.70.0\n\tgo.uber.org/zap v1.27.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgo.uber.org/multierr v1.11.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/zap/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=\ngo.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/zap/logger.go",
    "content": "package zap\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\tfiberlog \"github.com/gofiber/fiber/v3/log\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\nvar _ fiberlog.AllLogger[*zap.Logger] = (*LoggerConfig)(nil)\n\ntype LoggerConfig struct {\n\t// CoreConfigs allows users to configure Encoder, WriteSyncer, LevelEnabler configuration items provided by zapcore\n\t//\n\t// Optional. Default: LoggerConfigDefault\n\tCoreConfigs []CoreConfig\n\t// ZapOptions allow users to configure the zap.Option supplied by zap.\n\t//\n\t// Optional. Default: []zap.Option\n\tZapOptions []zap.Option\n\n\t// ExtraKeys allow users log extra values from context\n\t//\n\t// Optional. Default: []string\n\tExtraKeys []string\n\n\t// SetLogger sets *zap.Logger for fiberlog, if set, ZapOptions, CoreConfigs, SetLevel, SetOutput will be ignored\n\t//\n\t// Optional. Default: nil\n\tSetLogger *zap.Logger\n\n\tlogger *zap.Logger\n}\n\n// WithContext returns a new LoggerConfig with extra fields from context\nfunc (l *LoggerConfig) WithContext(ctx context.Context) fiberlog.CommonLogger {\n\tloggerOptions := l.logger.WithOptions(zap.AddCallerSkip(-1))\n\tnewLogger := &LoggerConfig{logger: loggerOptions}\n\n\tif len(l.ExtraKeys) > 0 {\n\t\tsugar := l.logger.Sugar()\n\t\tfor _, k := range l.ExtraKeys {\n\t\t\tvalue := ctx.Value(k)\n\t\t\tsugar = sugar.With(k, value)\n\t\t}\n\t\t// assign the new sugar to the new LoggerConfig\n\t\tnewLogger.logger = sugar.Desugar()\n\t}\n\treturn newLogger\n}\n\ntype CoreConfig struct {\n\tEncoder      zapcore.Encoder\n\tWriteSyncer  zapcore.WriteSyncer\n\tLevelEncoder zapcore.LevelEnabler\n}\n\n// LoggerConfigDefault is the default config\nvar LoggerConfigDefault = LoggerConfig{\n\tCoreConfigs: []CoreConfig{\n\t\t{\n\t\t\tEncoder:      zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),\n\t\t\tWriteSyncer:  zapcore.AddSync(os.Stdout),\n\t\t\tLevelEncoder: zap.NewAtomicLevelAt(zap.InfoLevel),\n\t\t},\n\t},\n\tZapOptions: []zap.Option{\n\t\tzap.AddCaller(),\n\t\tzap.AddCallerSkip(3),\n\t},\n}\n\nfunc loggerConfigDefault(config ...LoggerConfig) LoggerConfig {\n\t// Return default config if nothing provided\n\tif len(config) < 1 {\n\t\treturn LoggerConfigDefault\n\t}\n\n\t// Override default config\n\tcfg := config[0]\n\n\tif cfg.CoreConfigs == nil {\n\t\tcfg.CoreConfigs = LoggerConfigDefault.CoreConfigs\n\t}\n\n\tif cfg.SetLogger != nil {\n\t\tcfg.logger = cfg.SetLogger\n\t}\n\n\tif cfg.ZapOptions == nil {\n\t\tcfg.ZapOptions = LoggerConfigDefault.ZapOptions\n\t}\n\n\t// Remove duplicated extraKeys\n\tfor _, k := range cfg.ExtraKeys {\n\t\tif !contains(k, cfg.ExtraKeys) {\n\t\t\tcfg.ExtraKeys = append(cfg.ExtraKeys, k)\n\t\t}\n\t}\n\n\treturn cfg\n}\n\n// NewLogger creates a new zap logger adapter for fiberlog\nfunc NewLogger(config ...LoggerConfig) *LoggerConfig {\n\tcfg := loggerConfigDefault(config...)\n\n\t// Return logger if already exists\n\tif cfg.SetLogger != nil {\n\t\treturn &cfg\n\t}\n\n\tzapCores := make([]zapcore.Core, len(cfg.CoreConfigs))\n\tfor i, coreConfig := range cfg.CoreConfigs {\n\t\tzapCores[i] = zapcore.NewCore(coreConfig.Encoder, coreConfig.WriteSyncer, coreConfig.LevelEncoder)\n\t}\n\n\tcore := zapcore.NewTee(zapCores...)\n\tlogger := zap.New(core, cfg.ZapOptions...)\n\tcfg.logger = logger\n\n\treturn &cfg\n}\n\n// SetOutput sets the output destination for the logger.\nfunc (l *LoggerConfig) SetOutput(w io.Writer) {\n\tif l.SetLogger != nil {\n\t\tfiberlog.Warn(\"SetOutput is ignored when SetLogger is set\")\n\t\treturn\n\t}\n\tl.CoreConfigs[0].WriteSyncer = zapcore.AddSync(w)\n\tzapCores := make([]zapcore.Core, len(l.CoreConfigs))\n\tfor i, coreConfig := range l.CoreConfigs {\n\t\tzapCores[i] = zapcore.NewCore(coreConfig.Encoder, coreConfig.WriteSyncer, coreConfig.LevelEncoder)\n\t}\n\n\tcore := zapcore.NewTee(zapCores...)\n\tlogger := zap.New(core, l.ZapOptions...)\n\n\tl.logger = logger\n}\n\nfunc (l *LoggerConfig) SetLevel(lv fiberlog.Level) {\n\tif l.SetLogger != nil {\n\t\tfiberlog.Warn(\"SetLevel is ignored when SetLogger is set\")\n\t\treturn\n\t}\n\tvar level zapcore.Level\n\tswitch lv {\n\tcase fiberlog.LevelTrace, fiberlog.LevelDebug:\n\t\tlevel = zap.DebugLevel\n\tcase fiberlog.LevelInfo:\n\t\tlevel = zap.InfoLevel\n\tcase fiberlog.LevelWarn:\n\t\tlevel = zap.WarnLevel\n\tcase fiberlog.LevelError:\n\t\tlevel = zap.ErrorLevel\n\tcase fiberlog.LevelFatal:\n\t\tlevel = zap.FatalLevel\n\tcase fiberlog.LevelPanic:\n\t\tlevel = zap.PanicLevel\n\tdefault:\n\t\tlevel = zap.WarnLevel\n\t}\n\n\tl.CoreConfigs[0].LevelEncoder = level\n\tzapCores := make([]zapcore.Core, len(l.CoreConfigs))\n\tfor i, coreConfig := range l.CoreConfigs {\n\t\tzapCores[i] = zapcore.NewCore(coreConfig.Encoder, coreConfig.WriteSyncer, coreConfig.LevelEncoder)\n\t}\n\n\tcore := zapcore.NewTee(zapCores...)\n\tl.logger = zap.New(core, l.ZapOptions...)\n}\n\nfunc (l *LoggerConfig) Logf(level fiberlog.Level, format string, kvs ...interface{}) {\n\tlogger := l.logger.Sugar()\n\tswitch level {\n\tcase fiberlog.LevelTrace, fiberlog.LevelDebug:\n\t\tlogger.Debugf(format, kvs...)\n\tcase fiberlog.LevelInfo:\n\t\tlogger.Infof(format, kvs...)\n\tcase fiberlog.LevelWarn:\n\t\tlogger.Warnf(format, kvs...)\n\tcase fiberlog.LevelError:\n\t\tlogger.Errorf(format, kvs...)\n\tcase fiberlog.LevelFatal:\n\t\tlogger.Fatalf(format, kvs...)\n\tdefault:\n\t\tlogger.Warnf(format, kvs...)\n\t}\n}\n\nfunc (l *LoggerConfig) Trace(v ...interface{}) {\n\tl.Log(fiberlog.LevelTrace, v...)\n}\n\nfunc (l *LoggerConfig) Debug(v ...interface{}) {\n\tl.Log(fiberlog.LevelDebug, v...)\n}\n\nfunc (l *LoggerConfig) Info(v ...interface{}) {\n\tl.Log(fiberlog.LevelInfo, v...)\n}\n\nfunc (l *LoggerConfig) Warn(v ...interface{}) {\n\tl.Log(fiberlog.LevelWarn, v...)\n}\n\nfunc (l *LoggerConfig) Error(v ...interface{}) {\n\tl.Log(fiberlog.LevelError, v...)\n}\n\nfunc (l *LoggerConfig) Fatal(v ...interface{}) {\n\tl.Log(fiberlog.LevelFatal, v...)\n}\n\nfunc (l *LoggerConfig) Panic(v ...interface{}) {\n\tl.Log(fiberlog.LevelPanic, v...)\n}\n\nfunc (l *LoggerConfig) Tracef(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelTrace, format, v...)\n}\n\nfunc (l *LoggerConfig) Debugf(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelDebug, format, v...)\n}\n\nfunc (l *LoggerConfig) Infof(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelInfo, format, v...)\n}\n\nfunc (l *LoggerConfig) Warnf(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelWarn, format, v...)\n}\n\nfunc (l *LoggerConfig) Errorf(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelError, format, v...)\n}\n\nfunc (l *LoggerConfig) Fatalf(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelFatal, format, v...)\n}\n\nfunc (l *LoggerConfig) Panicf(format string, v ...interface{}) {\n\tl.Logf(fiberlog.LevelPanic, format, v...)\n}\n\nfunc (l *LoggerConfig) Tracew(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelTrace, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Debugw(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelDebug, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Infow(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelInfo, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Warnw(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelWarn, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Errorw(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelError, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Fatalw(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelFatal, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Panicw(msg string, keysAndValues ...interface{}) {\n\tl.Logw(fiberlog.LevelPanic, msg, keysAndValues...)\n}\n\nfunc (l *LoggerConfig) Log(level fiberlog.Level, kvs ...interface{}) {\n\tsugar := l.logger.Sugar()\n\tswitch level {\n\tcase fiberlog.LevelTrace, fiberlog.LevelDebug:\n\t\tsugar.Debug(kvs...)\n\tcase fiberlog.LevelInfo:\n\t\tsugar.Info(kvs...)\n\tcase fiberlog.LevelWarn:\n\t\tsugar.Warn(kvs...)\n\tcase fiberlog.LevelError:\n\t\tsugar.Error(kvs...)\n\tcase fiberlog.LevelFatal:\n\t\tsugar.Fatal(kvs...)\n\tcase fiberlog.LevelPanic:\n\t\tsugar.Panic(kvs...)\n\tdefault:\n\t\tsugar.Warn(kvs...)\n\t}\n}\n\nfunc (l *LoggerConfig) Logw(level fiberlog.Level, msg string, keyvals ...interface{}) {\n\tkeylen := len(keyvals)\n\tif keylen == 0 || keylen%2 != 0 {\n\t\tl.Logger().Warn(fmt.Sprint(\"Keyvalues must appear in pairs: \", keyvals))\n\t\treturn\n\t}\n\tdata := make([]zap.Field, 0, (keylen/2)+1)\n\tfor i := 0; i < keylen; i += 2 {\n\t\tdata = append(data, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1]))\n\t}\n\tswitch level {\n\tcase fiberlog.LevelTrace, fiberlog.LevelDebug:\n\t\tl.Logger().Debug(msg, data...)\n\tcase fiberlog.LevelInfo:\n\t\tl.Logger().Info(msg, data...)\n\tcase fiberlog.LevelWarn:\n\t\tl.Logger().Warn(msg, data...)\n\tcase fiberlog.LevelError:\n\t\tl.Logger().Error(msg, data...)\n\tcase fiberlog.LevelFatal:\n\t\tl.Logger().Fatal(msg, data...)\n\tdefault:\n\t\tl.Logger().Warn(msg, data...)\n\t}\n}\n\n// Sync flushes any buffered log entries.\nfunc (l *LoggerConfig) Sync() error {\n\treturn l.logger.Sync()\n}\n\n// Logger returns the underlying *zap.Logger when not using SetLogger\nfunc (l *LoggerConfig) Logger() *zap.Logger {\n\treturn l.logger\n}\n"
  },
  {
    "path": "v3/zap/logger_test.go",
    "content": "package zap\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gofiber/fiber/v3/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// testEncoderConfig encoder config for testing, copy from zap\nfunc testEncoderConfig() zapcore.EncoderConfig {\n\treturn zapcore.EncoderConfig{\n\t\tMessageKey:     \"msg\",\n\t\tLevelKey:       \"level\",\n\t\tNameKey:        \"name\",\n\t\tTimeKey:        \"ts\",\n\t\tCallerKey:      \"caller\",\n\t\tFunctionKey:    \"func\",\n\t\tStacktraceKey:  \"stacktrace\",\n\t\tLineEnding:     \"\\n\",\n\t\tEncodeTime:     zapcore.EpochTimeEncoder,\n\t\tEncodeDuration: zapcore.SecondsDurationEncoder,\n\t\tEncodeCaller:   zapcore.ShortCallerEncoder,\n\t}\n}\n\n// humanEncoderConfig copy from zap\nfunc humanEncoderConfig() zapcore.EncoderConfig {\n\tcfg := testEncoderConfig()\n\tcfg.EncodeTime = zapcore.ISO8601TimeEncoder\n\tcfg.EncodeLevel = zapcore.CapitalLevelEncoder\n\tcfg.EncodeDuration = zapcore.StringDurationEncoder\n\treturn cfg\n}\n\nfunc getWriteSyncer(file string) zapcore.WriteSyncer {\n\t_, err := os.Stat(file)\n\tif os.IsNotExist(err) {\n\t\t_ = os.MkdirAll(filepath.Dir(file), 0o744)\n\t}\n\n\tf, _ := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)\n\n\treturn zapcore.AddSync(f)\n}\n\n// TestCoreOption test zapcore config option\nfunc TestCoreOption(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\n\tdynamicLevel := zap.NewAtomicLevel()\n\n\tdynamicLevel.SetLevel(zap.InfoLevel)\n\n\tlogger := NewLogger(\n\t\tLoggerConfig{\n\t\t\tCoreConfigs: []CoreConfig{\n\t\t\t\t{\n\t\t\t\t\tEncoder:      zapcore.NewConsoleEncoder(humanEncoderConfig()),\n\t\t\t\t\tWriteSyncer:  zapcore.AddSync(os.Stdout),\n\t\t\t\t\tLevelEncoder: dynamicLevel,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tEncoder:      zapcore.NewJSONEncoder(humanEncoderConfig()),\n\t\t\t\t\tWriteSyncer:  getWriteSyncer(\"./all/log.log\"),\n\t\t\t\t\tLevelEncoder: zap.NewAtomicLevelAt(zapcore.DebugLevel),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tEncoder:     zapcore.NewJSONEncoder(humanEncoderConfig()),\n\t\t\t\t\tWriteSyncer: getWriteSyncer(\"./debug/log.log\"),\n\t\t\t\t\tLevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool {\n\t\t\t\t\t\treturn lev == zap.DebugLevel\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tEncoder:     zapcore.NewJSONEncoder(humanEncoderConfig()),\n\t\t\t\t\tWriteSyncer: getWriteSyncer(\"./info/log.log\"),\n\t\t\t\t\tLevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool {\n\t\t\t\t\t\treturn lev == zap.InfoLevel\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tEncoder:     zapcore.NewJSONEncoder(humanEncoderConfig()),\n\t\t\t\t\tWriteSyncer: getWriteSyncer(\"./warn/log.log\"),\n\t\t\t\t\tLevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool {\n\t\t\t\t\t\treturn lev == zap.WarnLevel\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tEncoder:     zapcore.NewJSONEncoder(humanEncoderConfig()),\n\t\t\t\t\tWriteSyncer: getWriteSyncer(\"./error/log.log\"),\n\t\t\t\t\tLevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool {\n\t\t\t\t\t\treturn lev >= zap.ErrorLevel\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\tdefer logger.Sync()\n\n\tlogger.SetOutput(buf)\n\n\tlogger.Debug(\"this is a debug log\")\n\t// test log level\n\tassert.False(t, strings.Contains(buf.String(), \"this is a debug log\"))\n\n\tlogger.Error(\"this is a warn log\")\n\t// test log level\n\tassert.True(t, strings.Contains(buf.String(), \"this is a warn log\"))\n\t// test console encoder result\n\tassert.True(t, strings.Contains(buf.String(), \"\\tERROR\\t\"))\n\n\tlogger.SetLevel(log.LevelDebug)\n\tlogger.Debug(\"this is a debug log\")\n\tassert.True(t, strings.Contains(buf.String(), \"this is a debug log\"))\n}\n\nfunc TestCoreConfigs(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\n\tlogger := NewLogger(LoggerConfig{\n\t\tCoreConfigs: []CoreConfig{\n\t\t\t{\n\t\t\t\tEncoder:      zapcore.NewConsoleEncoder(humanEncoderConfig()),\n\t\t\t\tLevelEncoder: zap.NewAtomicLevelAt(zap.WarnLevel),\n\t\t\t\tWriteSyncer:  zapcore.AddSync(buf),\n\t\t\t},\n\t\t},\n\t})\n\tdefer logger.Sync()\n\t// output to buffer\n\tlogger.SetOutput(buf)\n\n\tlogger.Infof(\"this is a info log %s\", \"msg\")\n\tassert.False(t, strings.Contains(buf.String(), \"this is a info log\"))\n\tlogger.Warnf(\"this is a warn log %s\", \"msg\")\n\tassert.True(t, strings.Contains(buf.String(), \"this is a warn log\"))\n}\n\n// TestCoreOptions test zapcore config option\nfunc TestZapOptions(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\n\tlogger := NewLogger(\n\t\tLoggerConfig{\n\t\t\tZapOptions: []zap.Option{\n\t\t\t\tzap.AddCaller(),\n\t\t\t},\n\t\t},\n\t)\n\tdefer logger.Sync()\n\n\tlogger.SetOutput(buf)\n\n\tlogger.Debug(\"this is a debug log\")\n\tassert.False(t, strings.Contains(buf.String(), \"this is a debug log\"))\n\n\tlogger.Error(\"this is a warn log\")\n\t// test caller in log result\n\tassert.True(t, strings.Contains(buf.String(), \"caller\"))\n}\n\nfunc TestWithContextCaller(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\tlogger := NewLogger(LoggerConfig{\n\t\tZapOptions: []zap.Option{\n\t\t\tzap.AddCaller(),\n\t\t\tzap.AddCallerSkip(3),\n\t\t},\n\t})\n\tlogger.SetOutput(buf)\n\n\tlogger.WithContext(context.Background()).Info(\"Hello, World!\")\n\tvar logStructMap map[string]interface{}\n\terr := json.Unmarshal(buf.Bytes(), &logStructMap)\n\tassert.Nil(t, err)\n\tvalue := logStructMap[\"caller\"]\n\tcaller, ok := value.(string)\n\tassert.True(t, ok)\n\tassert.Regexp(t, regexp.MustCompile(`zap/logger_test.go:\\d+`), caller)\n}\n\n// TestWithExtraKeys test WithExtraKeys option\nfunc TestWithExtraKeys(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\tlogger := NewLogger(LoggerConfig{\n\t\tExtraKeys: []string{\"requestId\"},\n\t})\n\tlogger.SetOutput(buf)\n\n\tctx := context.WithValue(context.Background(), \"requestId\", \"123\")\n\tlogger.WithContext(ctx).Infof(\"%s logger\", \"extra\")\n\n\tvar logStructMap map[string]interface{}\n\terr := json.Unmarshal(buf.Bytes(), &logStructMap)\n\tassert.Nil(t, err)\n\n\tvalue, ok := logStructMap[\"requestId\"]\n\n\tassert.True(t, ok)\n\tassert.Equal(t, value, \"123\")\n}\n\nfunc BenchmarkNormal(b *testing.B) {\n\tbuf := new(bytes.Buffer)\n\tlog := NewLogger()\n\tlog.SetOutput(buf)\n\tctx := context.Background()\n\tfor i := 0; i < b.N; i++ {\n\t\tlog.WithContext(ctx).Info(\"normal log\")\n\t}\n}\n\nfunc BenchmarkWithExtraKeys(b *testing.B) {\n\tbuf := new(bytes.Buffer)\n\tlogger := NewLogger(LoggerConfig{\n\t\tExtraKeys: []string{\"requestId\"},\n\t})\n\tlogger.SetOutput(buf)\n\tctx := context.WithValue(context.Background(), \"requestId\", \"123\")\n\tfor i := 0; i < b.N; i++ {\n\t\tlogger.WithContext(ctx).Info(\"normal logger\")\n\t}\n}\n\nfunc TestCustomField(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\tlogger := NewLogger()\n\tlog.SetLogger[*zap.Logger](logger)\n\tlog.SetOutput(buf)\n\tlog.Infow(\"\", \"test\", \"custom\")\n\tvar logStructMap map[string]interface{}\n\n\terr := json.Unmarshal(buf.Bytes(), &logStructMap)\n\n\tassert.Nil(t, err)\n\n\tvalue, ok := logStructMap[\"test\"]\n\n\tassert.True(t, ok)\n\tassert.Equal(t, value, \"custom\")\n}\n"
  },
  {
    "path": "v3/zap/zap.go",
    "content": "package zap\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/utils/v2\"\n\tutilsstrings \"github.com/gofiber/utils/v2/strings\"\n\t\"go.uber.org/zap\"\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 PID once\n\tpid := utils.FormatInt(int64(os.Getpid()))\n\n\t// Set variables\n\tvar (\n\t\tonce       sync.Once\n\t\terrHandler fiber.ErrorHandler\n\t)\n\n\tvar errPadding = 15\n\tvar latencyEnabled = contains(\"latency\", cfg.Fields)\n\n\t// put ignore uri into a map for faster match\n\tskipURIs := make(map[string]struct{})\n\tfor _, uri := range cfg.SkipURIs {\n\t\tskipURIs[uri] = struct{}{}\n\t}\n\n\t// Return new handler\n\treturn func(c fiber.Ctx) (err 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// skip uri\n\t\tif _, ok := skipURIs[c.Path()]; ok {\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 longested 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}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// override error handler\n\t\t\terrHandler = c.App().Config().ErrorHandler\n\t\t})\n\n\t\tvar start, stop time.Time\n\n\t\tif latencyEnabled {\n\t\t\tstart = time.Now()\n\t\t}\n\n\t\t// Handle request, store err for logging\n\t\tchainErr := c.Next()\n\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)\n\t\t\t}\n\t\t}\n\n\t\t// Set latency stop time\n\t\tif latencyEnabled {\n\t\t\tstop = time.Now()\n\t\t}\n\n\t\t// Check if the logger has the appropriate level\n\t\tvar (\n\t\t\ts     = c.Response().StatusCode()\n\t\t\tindex int\n\t\t)\n\t\tswitch {\n\t\tcase s >= 500:\n\t\t\t// error index is zero\n\t\tcase s >= 400:\n\t\t\tindex = 1\n\t\tdefault:\n\t\t\tindex = 2\n\t\t}\n\t\tlevelIndex := index\n\t\tif levelIndex >= len(cfg.Levels) {\n\t\t\tlevelIndex = len(cfg.Levels) - 1\n\t\t}\n\t\tmessageIndex := index\n\t\tif messageIndex >= len(cfg.Messages) {\n\t\t\tmessageIndex = len(cfg.Messages) - 1\n\t\t}\n\n\t\tce := cfg.Logger.Check(cfg.Levels[levelIndex], cfg.Messages[messageIndex])\n\t\tif ce == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Add fields\n\t\tfields := make([]zap.Field, 0, len(cfg.Fields)+1)\n\t\tfields = append(fields, zap.Error(err))\n\n\t\tif cfg.FieldsFunc != nil {\n\t\t\tfields = append(fields, cfg.FieldsFunc(c)...)\n\t\t}\n\n\t\tfor _, field := range cfg.Fields {\n\t\t\tswitch field {\n\t\t\tcase \"referer\":\n\t\t\t\tfields = append(fields, zap.String(\"referer\", c.Get(fiber.HeaderReferer)))\n\t\t\tcase \"protocol\":\n\t\t\t\tfields = append(fields, zap.String(\"protocol\", c.Protocol()))\n\t\t\tcase \"pid\":\n\t\t\t\tfields = append(fields, zap.String(\"pid\", pid))\n\t\t\tcase \"port\":\n\t\t\t\tfields = append(fields, zap.String(\"port\", c.Port()))\n\t\t\tcase \"ip\":\n\t\t\t\tfields = append(fields, zap.String(\"ip\", c.IP()))\n\t\t\tcase \"ips\":\n\t\t\t\tfields = append(fields, zap.String(\"ips\", c.Get(fiber.HeaderXForwardedFor)))\n\t\t\tcase \"host\":\n\t\t\t\tfields = append(fields, zap.String(\"host\", c.Hostname()))\n\t\t\tcase \"path\":\n\t\t\t\tfields = append(fields, zap.String(\"path\", c.Path()))\n\t\t\tcase \"url\":\n\t\t\t\tfields = append(fields, zap.String(\"url\", c.OriginalURL()))\n\t\t\tcase \"ua\":\n\t\t\t\tfields = append(fields, zap.String(\"ua\", c.Get(fiber.HeaderUserAgent)))\n\t\t\tcase \"latency\":\n\t\t\t\tfields = append(fields, zap.String(\"latency\", stop.Sub(start).String()))\n\t\t\tcase \"status\":\n\t\t\t\tfields = append(fields, zap.Int(\"status\", c.Response().StatusCode()))\n\t\t\tcase \"resBody\":\n\t\t\t\tif cfg.SkipResBody == nil || !cfg.SkipResBody(c) {\n\t\t\t\t\tif cfg.GetResBody == nil {\n\t\t\t\t\t\tfields = append(fields, zap.ByteString(\"resBody\", c.Response().Body()))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfields = append(fields, zap.ByteString(\"resBody\", cfg.GetResBody(c)))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"queryParams\":\n\t\t\t\tfields = append(fields, zap.String(\"queryParams\", c.Request().URI().QueryArgs().String()))\n\t\t\tcase \"body\":\n\t\t\t\tif cfg.SkipBody == nil || !cfg.SkipBody(c) {\n\t\t\t\t\tfields = append(fields, zap.ByteString(\"body\", c.Body()))\n\t\t\t\t}\n\t\t\tcase \"bytesReceived\":\n\t\t\t\tfields = append(fields, zap.Int(\"bytesReceived\", len(c.Request().Body())))\n\t\t\tcase \"bytesSent\":\n\t\t\t\tfields = append(fields, zap.Int(\"bytesSent\", len(c.Response().Body())))\n\t\t\tcase \"route\":\n\t\t\t\tfields = append(fields, zap.String(\"route\", c.Route().Path))\n\t\t\tcase \"method\":\n\t\t\t\tfields = append(fields, zap.String(\"method\", c.Method()))\n\t\t\tcase \"requestId\":\n\t\t\t\tfields = append(fields, zap.String(\"requestId\", c.GetRespHeader(fiber.HeaderXRequestID)))\n\t\t\tcase \"error\":\n\t\t\t\tif chainErr != nil {\n\t\t\t\t\tfields = append(fields, zap.String(\"error\", chainErr.Error()))\n\t\t\t\t}\n\t\t\tcase \"reqHeaders\":\n\t\t\t\tfor header, values := range c.GetReqHeaders() {\n\t\t\t\t\tif len(values) == 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tsanitized := sanitizeHeaderValues(header, values)\n\n\t\t\t\t\tif len(sanitized) == 1 {\n\t\t\t\t\t\tfields = append(fields, zap.String(header, sanitized[0]))\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tfields = append(fields, zap.Strings(header, sanitized))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tce.Write(fields...)\n\n\t\treturn nil\n\t}\n}\n\nfunc contains(needle string, slice []string) bool {\n\tfor _, e := range slice {\n\t\tif e == needle {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nvar sensitiveRequestHeaders = map[string]struct{}{\n\t\"authorization\":       {},\n\t\"proxy-authorization\": {},\n\t\"cookie\":              {},\n\t\"x-api-key\":           {},\n\t\"x-auth-token\":        {},\n}\n\nfunc sanitizeHeaderValues(header string, values []string) []string {\n\tif len(values) == 0 {\n\t\treturn values\n\t}\n\n\tif _, ok := sensitiveRequestHeaders[utilsstrings.ToLower(header)]; !ok {\n\t\treturn values\n\t}\n\n\tsanitized := make([]string, len(values))\n\tfor i := range sanitized {\n\t\tsanitized[i] = \"[REDACTED]\"\n\t}\n\n\treturn sanitized\n}\n"
  },
  {
    "path": "v3/zap/zap_test.go",
    "content": "package zap\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/valyala/fasthttp\"\n\t\"go.uber.org/zap\"\n\t\"go.uber.org/zap/zapcore\"\n\t\"go.uber.org/zap/zaptest/observer\"\n)\n\nfunc setupLogsCapture() (*zap.Logger, *observer.ObservedLogs) {\n\tcore, logs := observer.New(zap.InfoLevel)\n\treturn zap.New(core), logs\n}\n\nfunc Test_GetResBody(t *testing.T) {\n\tvar readableResBody = \"this is readable response body\"\n\n\tvar app = fiber.New()\n\tvar logger, logs = setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tGetResBody: func(c fiber.Ctx) []byte {\n\t\t\treturn []byte(readableResBody)\n\t\t},\n\t\tFields: []string{\"resBody\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"------this is unreadable resp------\")\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, readableResBody, logs.All()[0].ContextMap()[\"resBody\"])\n}\n\n// go test -run Test_SkipBody\nfunc Test_SkipBody(t *testing.T) {\n\tlogger, logs := setupLogsCapture()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSkipBody: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t\tLogger: logger,\n\t\tFields: []string{\"pid\", \"body\"},\n\t}))\n\n\tbody := bytes.NewReader([]byte(\"this is test\"))\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", body))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\t_, ok := logs.All()[0].ContextMap()[\"body\"]\n\tassert.Equal(t, false, ok)\n}\n\n// go test -run Test_SkipResBody\nfunc Test_SkipResBody(t *testing.T) {\n\tlogger, logs := setupLogsCapture()\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSkipResBody: func(_ fiber.Ctx) bool {\n\t\t\treturn true\n\t\t},\n\t\tLogger: logger,\n\t\tFields: []string{\"pid\", \"resBody\"},\n\t}))\n\n\tbody := bytes.NewReader([]byte(\"this is test\"))\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", body))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\t_, ok := logs.All()[0].ContextMap()[\"resBody\"]\n\tassert.Equal(t, false, ok)\n}\n\n// go test -run Test_Logger\nfunc Test_Logger(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"pid\", \"latency\", \"error\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn errors.New(\"some random error\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\tassert.Equal(t, \"some random error\", logs.All()[0].Context[3].String)\n}\n\n// go test -run Test_Logger_Next\nfunc Test_Logger_Next(t *testing.T) {\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(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\n// go test -run Test_Logger_All\nfunc Test_Logger_All(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"protocol\", \"pid\", \"body\", \"ip\", \"host\", \"url\", \"route\", \"method\", \"resBody\", \"queryParams\", \"bytesReceived\", \"bytesSent\"},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/?foo=bar\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"body\":          \"\",\n\t\t\"ip\":            \"0.0.0.0\",\n\t\t\"host\":          \"example.com\",\n\t\t\"url\":           \"/?foo=bar\",\n\t\t\"method\":        \"GET\",\n\t\t\"route\":         \"/\",\n\t\t\"protocol\":      \"HTTP/1.1\",\n\t\t\"pid\":           strconv.Itoa(os.Getpid()),\n\t\t\"queryParams\":   \"foo=bar\",\n\t\t\"resBody\":       \"Not Found\",\n\t\t\"bytesReceived\": int64(0),\n\t\t\"bytesSent\":     int64(9),\n\t}\n\n\tassert.Equal(t, expected, logs.All()[0].ContextMap())\n}\n\n// go test -run Test_Query_Params\nfunc Test_Query_Params(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"queryParams\"},\n\t}))\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/?foo=bar&baz=moz\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\texpected := \"foo=bar&baz=moz\"\n\tassert.Equal(t, expected, logs.All()[0].Context[1].String)\n}\n\n// go test -run Test_Response_Body\nfunc Test_Response_Body(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"resBody\"},\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(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\n\texpectedGetResponse := \"Sample response body\"\n\tassert.Equal(t, expectedGetResponse, logs.All()[0].ContextMap()[\"resBody\"])\n\n\t_, err = app.Test(httptest.NewRequest(\"POST\", \"/test\", nil))\n\tassert.Equal(t, nil, err)\n\n\texpectedPostResponse := \"Post in test\"\n\tt.Log(logs.All())\n\tassert.Equal(t, expectedPostResponse, logs.All()[1].ContextMap()[\"resBody\"])\n}\n\n// go test -run Test_Logger_AppendUint\nfunc Test_Logger_AppendUint(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"bytesReceived\", \"bytesSent\", \"status\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\toutput := logs.All()[0].ContextMap()\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tassert.Equal(t, \"0 5 200\", fmt.Sprintf(\"%d %d %d\", output[\"bytesReceived\"], output[\"bytesSent\"], output[\"status\"]))\n}\n\n// go test -run Test_Logger_Data_Race -race\nfunc Test_Logger_Data_Race(t *testing.T) {\n\tapp := fiber.New()\n\tlogger := zap.NewExample()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"bytesReceived\", \"bytesSent\", \"status\"},\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.Add(1)\n\tgo func() {\n\t\tresp1, err1 = app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\t\twg.Done()\n\t}()\n\tresp2, err2 = app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\twg.Wait()\n\n\tassert.Equal(t, nil, err1)\n\tassert.Equal(t, fiber.StatusOK, resp1.StatusCode)\n\tassert.Equal(t, nil, err2)\n\tassert.Equal(t, fiber.StatusOK, resp2.StatusCode)\n}\n\n// go test -v -run=^$ -bench=Benchmark_Logger -benchmem -count=4\nfunc Benchmark_Logger(b *testing.B) {\n\tapp := fiber.New()\n\n\tapp.Use(New())\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(\"GET\")\n\tfctx.Request.SetRequestURI(\"/\")\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor n := 0; n < b.N; n++ {\n\t\th(fctx)\n\t}\n\n\tassert.Equal(b, 200, fctx.Response.Header.StatusCode())\n}\n\n// go test -run Test_Request_Id\nfunc Test_Request_Id(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"requestId\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.Add(fiber.HeaderXRequestID, \"bf985e8e-6a32-42ec-8e50-05a21db8f0e4\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tassert.Equal(t, \"bf985e8e-6a32-42ec-8e50-05a21db8f0e4\", logs.All()[0].Context[1].String)\n}\n\n// go test -run Test_Skip_URIs\nfunc Test_Skip_URIs(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger:   logger,\n\t\tSkipURIs: []string{\"/ignore_logging\"},\n\t}))\n\n\tapp.Get(\"/ignore_logging\", func(c fiber.Ctx) error {\n\t\treturn errors.New(\"no log\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/ignore_logging\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\tassert.Equal(t, 0, len(logs.All()))\n}\n\n// go test -run Test_Req_Headers\nfunc Test_Req_Headers(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"reqHeaders\"},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\texpected := map[string]interface{}{\n\t\t\"Host\": \"example.com\",\n\t\t\"Baz\":  \"foo\",\n\t\t\"Foo\":  \"bar\",\n\t}\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\treq.Header.Add(\"foo\", \"bar\")\n\treq.Header.Add(\"baz\", \"foo\")\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tassert.Equal(t, expected, logs.All()[0].ContextMap())\n}\n\n// go test -run Test_LoggerLevelsAndMessages\nfunc Test_LoggerLevelsAndMessages(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tlevels := []zapcore.Level{zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel}\n\tmessages := []string{\"server error\", \"client error\", \"success\"}\n\tapp.Use(New(Config{\n\t\tLogger:   logger,\n\t\tMessages: messages,\n\t\tLevels:   levels,\n\t}))\n\n\tapp.Get(\"/200\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusOK)\n\t\treturn nil\n\t})\n\tapp.Get(\"/400\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusBadRequest)\n\t\treturn nil\n\t})\n\tapp.Get(\"/500\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusInternalServerError)\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/500\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\tassert.Equal(t, levels[0], logs.All()[0].Level)\n\tassert.Equal(t, messages[0], logs.All()[0].Message)\n\tresp, err = app.Test(httptest.NewRequest(\"GET\", \"/400\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\tassert.Equal(t, levels[1], logs.All()[1].Level)\n\tassert.Equal(t, messages[1], logs.All()[1].Message)\n\tresp, err = app.Test(httptest.NewRequest(\"GET\", \"/200\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tassert.Equal(t, levels[2], logs.All()[2].Level)\n\tassert.Equal(t, messages[2], logs.All()[2].Message)\n}\n\n// go test -run Test_LoggerLevelsAndMessagesSingle\nfunc Test_LoggerLevelsAndMessagesSingle(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tlevels := []zapcore.Level{zapcore.ErrorLevel}\n\tmessages := []string{\"server error\"}\n\tapp.Use(New(Config{\n\t\tLogger:   logger,\n\t\tMessages: messages,\n\t\tLevels:   levels,\n\t}))\n\n\tapp.Get(\"/200\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusOK)\n\t\treturn nil\n\t})\n\tapp.Get(\"/400\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusBadRequest)\n\t\treturn nil\n\t})\n\tapp.Get(\"/500\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusInternalServerError)\n\t\treturn nil\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/500\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\tassert.Equal(t, levels[0], logs.All()[0].Level)\n\tassert.Equal(t, messages[0], logs.All()[0].Message)\n\tresp, err = app.Test(httptest.NewRequest(\"GET\", \"/400\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusBadRequest, resp.StatusCode)\n\tassert.Equal(t, levels[0], logs.All()[1].Level)\n\tassert.Equal(t, messages[0], logs.All()[1].Message)\n\tresp, err = app.Test(httptest.NewRequest(\"GET\", \"/200\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\tassert.Equal(t, levels[0], logs.All()[2].Level)\n\tassert.Equal(t, messages[0], logs.All()[2].Message)\n}\n\n// go test -run Test_Fields_Func\nfunc Test_Fields_Func(t *testing.T) {\n\tapp := fiber.New()\n\tlogger, logs := setupLogsCapture()\n\n\tapp.Use(New(Config{\n\t\tLogger: logger,\n\t\tFields: []string{\"protocol\", \"pid\", \"body\", \"ip\", \"host\", \"url\", \"route\", \"method\", \"resBody\", \"queryParams\", \"bytesReceived\", \"bytesSent\"},\n\t\tFieldsFunc: func(c fiber.Ctx) []zap.Field {\n\t\t\treturn []zap.Field{zap.String(\"test.custom.field\", \"test\")}\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"body\":              \"\",\n\t\t\"ip\":                \"0.0.0.0\",\n\t\t\"host\":              \"example.com\",\n\t\t\"url\":               \"/\",\n\t\t\"method\":            \"GET\",\n\t\t\"route\":             \"/\",\n\t\t\"protocol\":          \"HTTP/1.1\",\n\t\t\"pid\":               strconv.Itoa(os.Getpid()),\n\t\t\"queryParams\":       \"\",\n\t\t\"resBody\":           \"hello\",\n\t\t\"bytesReceived\":     int64(0),\n\t\t\"bytesSent\":         int64(5),\n\t\t\"test.custom.field\": \"test\",\n\t}\n\n\tassert.Equal(t, expected, logs.All()[0].ContextMap())\n}\n"
  },
  {
    "path": "v3/zerolog/README.md",
    "content": "---\nid: zerolog\n---\n\n# Zerolog\n\n![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*zerolog*)\n[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)\n![Test](https://github.com/gofiber/contrib/workflows/Test%20zerolog/badge.svg)\n\n[Zerolog](https://zerolog.io/) logging support for Fiber.\n\n\n**Compatible with Fiber v3.**\n\n## Go version support\n\nWe only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information.\n\n## Install\n\n```sh\ngo get -u github.com/gofiber/fiber/v3\ngo get -u github.com/gofiber/contrib/v3/zerolog\ngo get -u github.com/rs/zerolog/log\n```\n\n## Signature\n\n```go\nzerolog.New(config ...zerolog.Config) fiber.Handler\n```\n\n## Config\n\n| Property        | Type                                | Description                                                                                                                                                                                                                                        | Default                                                                  |\n|:----------------|:------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------|\n| Next            | `func(fiber.Ctx) bool`              | Define a function to skip this middleware when it returns true.                                                                                                                                                                                   | `nil`                                                                    |\n| Logger          | `*zerolog.Logger`                   | Add a custom zerolog logger.                                                                                                                                                                                                                      | `zerolog.New(os.Stderr).With().Timestamp().Logger()`                     |\n| GetLogger       | `func(fiber.Ctx) zerolog.Logger`    | Get a custom zerolog logger. If set, the returned logger replaces `Logger`.                                                                                                                                                                       | `nil`                                                                    |\n| Fields          | `[]string`                          | Add the fields you want to log.                                                                                                                                                                                                                   | `[]string{\"latency\", \"status\", \"method\", \"url\", \"error\"}`               |\n| SkipField       | `func(string, fiber.Ctx) bool`      | Skip logging a field when it returns true.                                                                                                                                                                                                         | `nil`                                                                    |\n| SkipHeader      | `func(string, fiber.Ctx) bool`      | Skip logging a header when it returns true.                                                                                                                                                                                                        | `nil`                                                                    |\n| WrapHeaders     | `bool`                              | Wrap headers into a dictionary.<br />If false: `{\"method\":\"POST\", \"header-key\":\"header value\"}`<br />If true: `{\"method\":\"POST\", \"reqHeaders\":{\"header-key\":\"header value\"}}`                                                                  | `false`                                                                  |\n| FieldsSnakeCase | `bool`                              | Use snake case for `FieldResBody`, `FieldQueryParams`, `FieldBytesReceived`, `FieldBytesSent`, `FieldRequestID`, `FieldReqHeaders`, `FieldResHeaders`.<br />If false: `{\"method\":\"POST\", \"resBody\":\"v\", \"queryParams\":\"v\"}`<br />If true: `{\"method\":\"POST\", \"res_body\":\"v\", \"query_params\":\"v\"}` | `false`                                                                  |\n| Messages        | `[]string`                          | Custom response messages.                                                                                                                                                                                                                          | `[]string{\"Server error\", \"Client error\", \"Success\"}`                    |\n| Levels          | `[]zerolog.Level`                   | Custom response levels.                                                                                                                                                                                                                            | `[]zerolog.Level{zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel}` |\n| GetResBody      | `func(c fiber.Ctx) []byte`          | Define a function to get the response body when it returns non-nil.<br />For example, with compress middleware the body can be unreadable; `GetResBody` lets you provide a readable body.                                                      | `nil`                                                                    |\n\n## Example\n\n```go\npackage main\n\nimport (\n    \"os\"\n\n    middleware \"github.com/gofiber/contrib/v3/zerolog\"\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/rs/zerolog\"\n)\n\nfunc main() {\n    app := fiber.New()\n    logger := zerolog.New(os.Stderr).With().Timestamp().Logger()\n\n    app.Use(middleware.New(middleware.Config{\n        Logger: &logger,\n    }))\n\n    app.Get(\"/\", func(c fiber.Ctx) error {\n        return c.SendString(\"Hello, World!\")\n    })\n\n    if err := app.Listen(\":3000\"); err != nil {\n        logger.Fatal().Err(err).Msg(\"Fiber app error\")\n    }\n}\n```\n"
  },
  {
    "path": "v3/zerolog/config.go",
    "content": "package zerolog\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/rs/zerolog\"\n)\n\nconst (\n\tFieldReferer       = \"referer\"\n\tFieldProtocol      = \"protocol\"\n\tFieldPID           = \"pid\"\n\tFieldPort          = \"port\"\n\tFieldIP            = \"ip\"\n\tFieldIPs           = \"ips\"\n\tFieldHost          = \"host\"\n\tFieldPath          = \"path\"\n\tFieldURL           = \"url\"\n\tFieldUserAgent     = \"ua\"\n\tFieldLatency       = \"latency\"\n\tFieldStatus        = \"status\"\n\tFieldResBody       = \"resBody\"\n\tFieldQueryParams   = \"queryParams\"\n\tFieldBody          = \"body\"\n\tFieldBytesReceived = \"bytesReceived\"\n\tFieldBytesSent     = \"bytesSent\"\n\tFieldRoute         = \"route\"\n\tFieldMethod        = \"method\"\n\tFieldRequestID     = \"requestId\"\n\tFieldError         = \"error\"\n\tFieldReqHeaders    = \"reqHeaders\"\n\tFieldResHeaders    = \"resHeaders\"\n\n\tfieldResBody_       = \"res_body\"\n\tfieldQueryParams_   = \"query_params\"\n\tfieldBytesReceived_ = \"bytes_received\"\n\tfieldBytesSent_     = \"bytes_sent\"\n\tfieldRequestID_     = \"request_id\"\n\tfieldReqHeaders_    = \"req_headers\"\n\tfieldResHeaders_    = \"res_headers\"\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// SkipField defines a function that returns true if a specific field should be skipped from logging.\n\t//\n\t// Optional. Default: nil\n\tSkipField func(field string, c fiber.Ctx) bool\n\n\t// GetResBody defines a function to get a custom response body.\n\t// e.g. when using compress middleware, the original response body may be unreadable.\n\t// You can use GetResBody to provide a readable body.\n\t//\n\t// Optional. Default: nil\n\tGetResBody func(c fiber.Ctx) []byte\n\n\t// Add a custom zerolog logger.\n\t//\n\t// Optional. Default: zerolog.New(os.Stderr).With().Timestamp().Logger()\n\tLogger *zerolog.Logger\n\n\t// GetLogger defines a function to get a custom zerolog logger.\n\t// e.g. when creating a new logger for each request.\n\t//\n\t// GetLogger will override Logger.\n\t//\n\t// Optional. Default: nil\n\tGetLogger func(c fiber.Ctx) zerolog.Logger\n\n\t// Add the fields you want to log.\n\t//\n\t// Optional. Default: {\"ip\", \"latency\", \"status\", \"method\", \"url\", \"error\"}\n\tFields []string\n\n\t// Defines a function that returns true if a header should not be logged.\n\t// Only relevant if `FieldReqHeaders` and/or `FieldResHeaders` are logged.\n\t//\n\t// Optional. Default: nil\n\tSkipHeader func(header string, c fiber.Ctx) bool\n\n\t// Wrap headers into a dictionary.\n\t// If false: {\"method\":\"POST\", \"header-key\":\"header value\"}\n\t// If true: {\"method\":\"POST\", \"reqHeaders\": {\"header-key\":\"header value\"}}\n\t//\n\t// Optional. Default: false\n\tWrapHeaders bool\n\n\t// Use snake case for fields: FieldResBody, FieldQueryParams, FieldBytesReceived, FieldBytesSent, FieldRequestID, FieldReqHeaders, FieldResHeaders.\n\t// If false: {\"method\":\"POST\", \"resBody\":\"v\", \"queryParams\":\"v\"}\n\t// If true: {\"method\":\"POST\", \"res_body\":\"v\", \"query_params\":\"v\"}\n\t//\n\t// Optional. Default: false\n\tFieldsSnakeCase bool\n\n\t// Custom response messages.\n\t// Response codes >= 500 will be logged with Messages[0].\n\t// Response codes >= 400 will be logged with Messages[1].\n\t// Other response codes will be logged with Messages[2].\n\t// You can specify fewer than 3 messages, but you must specify at least 1.\n\t// Specifying more than 3 messages is useless.\n\t//\n\t// Optional. Default: {\"Server error\", \"Client error\", \"Success\"}\n\tMessages []string\n\n\t// Custom response levels.\n\t// Response codes >= 500 will be logged with Levels[0].\n\t// Response codes >= 400 will be logged with Levels[1].\n\t// Other response codes will be logged with Levels[2].\n\t// You can specify fewer than 3 levels, but you must specify at least 1.\n\t// Specifying more than 3 levels is useless.\n\t//\n\t// Optional. Default: {zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel}\n\tLevels []zerolog.Level\n}\n\nfunc (c *Config) loggerCtx(fc fiber.Ctx) zerolog.Context {\n\tif c.GetLogger != nil {\n\t\treturn c.GetLogger(fc).With()\n\t}\n\n\treturn c.Logger.With()\n}\n\nfunc (c *Config) logger(fc fiber.Ctx, latency time.Duration, err error) zerolog.Logger {\n\tzc := c.loggerCtx(fc)\n\n\tfor _, field := range c.Fields {\n\t\tif c.SkipField != nil && c.SkipField(field, fc) {\n\t\t\tcontinue\n\t\t}\n\t\tswitch field {\n\t\tcase FieldReferer:\n\t\t\tzc = zc.Str(field, fc.Get(fiber.HeaderReferer))\n\t\tcase FieldProtocol:\n\t\t\tzc = zc.Str(field, fc.Protocol())\n\t\tcase FieldPID:\n\t\t\tzc = zc.Int(field, os.Getpid())\n\t\tcase FieldPort:\n\t\t\tzc = zc.Str(field, fc.Port())\n\t\tcase FieldIP:\n\t\t\tzc = zc.Str(field, fc.IP())\n\t\tcase FieldIPs:\n\t\t\tzc = zc.Str(field, fc.Get(fiber.HeaderXForwardedFor))\n\t\tcase FieldHost:\n\t\t\tzc = zc.Str(field, fc.Hostname())\n\t\tcase FieldPath:\n\t\t\tzc = zc.Str(field, fc.Path())\n\t\tcase FieldURL:\n\t\t\tzc = zc.Str(field, fc.OriginalURL())\n\t\tcase FieldUserAgent:\n\t\t\tzc = zc.Str(field, fc.Get(fiber.HeaderUserAgent))\n\t\tcase FieldLatency:\n\t\t\tzc = zc.Str(field, latency.String())\n\t\tcase FieldStatus:\n\t\t\tzc = zc.Int(field, fc.Response().StatusCode())\n\t\tcase FieldBody:\n\t\t\tzc = zc.Str(field, string(fc.Body()))\n\t\tcase FieldResBody:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldResBody_\n\t\t\t}\n\t\t\tresBody := fc.Response().Body()\n\t\t\tif c.GetResBody != nil {\n\t\t\t\tif customResBody := c.GetResBody(fc); customResBody != nil {\n\t\t\t\t\tresBody = customResBody\n\t\t\t\t}\n\t\t\t}\n\t\t\tzc = zc.Str(field, string(resBody))\n\t\tcase FieldQueryParams:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldQueryParams_\n\t\t\t}\n\t\t\tzc = zc.Stringer(field, fc.Request().URI().QueryArgs())\n\t\tcase FieldBytesReceived:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldBytesReceived_\n\t\t\t}\n\t\t\tzc = zc.Int(field, len(fc.Request().Body()))\n\t\tcase FieldBytesSent:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldBytesSent_\n\t\t\t}\n\t\t\tzc = zc.Int(field, len(fc.Response().Body()))\n\t\tcase FieldRoute:\n\t\t\tzc = zc.Str(field, fc.Route().Path)\n\t\tcase FieldMethod:\n\t\t\tzc = zc.Str(field, fc.Method())\n\t\tcase FieldRequestID:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldRequestID_\n\t\t\t}\n\t\t\tzc = zc.Str(field, fc.GetRespHeader(fiber.HeaderXRequestID))\n\t\tcase FieldError:\n\t\t\tif err != nil {\n\t\t\t\tzc = zc.Err(err)\n\t\t\t}\n\t\tcase FieldReqHeaders:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldReqHeaders_\n\t\t\t}\n\t\t\tif c.WrapHeaders {\n\t\t\t\tdict := zerolog.Dict()\n\t\t\t\tfor header, values := range fc.GetReqHeaders() {\n\t\t\t\t\tif len(values) == 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif c.SkipHeader != nil && c.SkipHeader(header, fc) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(values) == 1 {\n\t\t\t\t\t\tdict.Str(header, values[0])\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tdict.Strs(header, values)\n\t\t\t\t}\n\t\t\t\tzc = zc.Dict(field, dict)\n\t\t\t} else {\n\t\t\t\tfor header, values := range fc.GetReqHeaders() {\n\t\t\t\t\tif len(values) == 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif c.SkipHeader != nil && c.SkipHeader(header, fc) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(values) == 1 {\n\t\t\t\t\t\tzc = zc.Str(header, values[0])\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tzc = zc.Strs(header, values)\n\t\t\t\t}\n\t\t\t}\n\t\tcase FieldResHeaders:\n\t\t\tif c.FieldsSnakeCase {\n\t\t\t\tfield = fieldResHeaders_\n\t\t\t}\n\t\t\tif c.WrapHeaders {\n\t\t\t\tdict := zerolog.Dict()\n\t\t\t\tfor header, values := range fc.GetRespHeaders() {\n\t\t\t\t\tif len(values) == 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif c.SkipHeader != nil && c.SkipHeader(header, fc) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(values) == 1 {\n\t\t\t\t\t\tdict.Str(header, values[0])\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tdict.Strs(header, values)\n\t\t\t\t}\n\t\t\t\tzc = zc.Dict(field, dict)\n\t\t\t} else {\n\t\t\t\tfor header, values := range fc.GetRespHeaders() {\n\t\t\t\t\tif len(values) == 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif c.SkipHeader != nil && c.SkipHeader(header, fc) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(values) == 1 {\n\t\t\t\t\t\tzc = zc.Str(header, values[0])\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tzc = zc.Strs(header, values)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn zc.Logger()\n}\n\nvar logger = zerolog.New(os.Stderr).With().Timestamp().Logger()\n\n// ConfigDefault is the default config\nvar ConfigDefault = Config{\n\tNext:     nil,\n\tLogger:   &logger,\n\tFields:   []string{FieldIP, FieldLatency, FieldStatus, FieldMethod, FieldURL, FieldError},\n\tMessages: []string{\"Server error\", \"Client error\", \"Success\"},\n\tLevels:   []zerolog.Level{zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel},\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.Logger == nil {\n\t\tcfg.Logger = ConfigDefault.Logger\n\t}\n\n\tif cfg.Fields == nil {\n\t\tcfg.Fields = ConfigDefault.Fields\n\t}\n\n\tif cfg.Messages == nil {\n\t\tcfg.Messages = ConfigDefault.Messages\n\t}\n\n\tif cfg.Levels == nil {\n\t\tcfg.Levels = ConfigDefault.Levels\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "v3/zerolog/go.mod",
    "content": "module github.com/gofiber/contrib/v3/zerolog\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/gofiber/fiber/v3 v3.1.0\n\tgithub.com/rs/zerolog v1.35.0\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.2.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/gofiber/schema v1.7.1 // indirect\n\tgithub.com/gofiber/utils/v2 v2.0.3 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.21 // indirect\n\tgithub.com/philhofer/fwd v1.2.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/tinylib/msgp v1.6.4 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasthttp v1.70.0 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/text v0.36.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "v3/zerolog/go.sum",
    "content": "github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=\ngithub.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=\ngithub.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=\ngithub.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=\ngithub.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=\ngithub.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=\ngithub.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI=\ngithub.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=\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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\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.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=\ngithub.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=\ngithub.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=\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.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=\ngithub.com/tinylib/msgp v1.6.4/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.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=\ngithub.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=\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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\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": "v3/zerolog/zerolog.go",
    "content": "package zerolog\n\nimport (\n\t\"time\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/rs/zerolog\"\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\t// Handle request, store err for logging\n\t\tchainErr := c.Next()\n\t\tif chainErr != nil {\n\t\t\t// Manually call error handler\n\t\t\tif err := c.App().ErrorHandler(c, chainErr); err != nil {\n\t\t\t\t_ = c.SendStatus(fiber.StatusInternalServerError)\n\t\t\t}\n\t\t}\n\n\t\tlatency := time.Since(start)\n\n\t\tstatus := c.Response().StatusCode()\n\n\t\tindex := 0\n\t\tswitch {\n\t\tcase status >= 500:\n\t\t\t// error index is zero\n\t\tcase status >= 400:\n\t\t\tindex = 1\n\t\tdefault:\n\t\t\tindex = 2\n\t\t}\n\n\t\tlevelIndex := index\n\t\tif levelIndex >= len(cfg.Levels) {\n\t\t\tlevelIndex = len(cfg.Levels) - 1\n\t\t}\n\t\tlevel := cfg.Levels[levelIndex]\n\n\t\t// no log\n\t\tif level == zerolog.NoLevel || level == zerolog.Disabled {\n\t\t\treturn nil\n\t\t}\n\n\t\tmessageIndex := index\n\t\tif messageIndex >= len(cfg.Messages) {\n\t\t\tmessageIndex = len(cfg.Messages) - 1\n\t\t}\n\t\tmessage := cfg.Messages[messageIndex]\n\n\t\tlogger := cfg.logger(c, latency, chainErr)\n\t\tctx := c\n\n\t\tswitch level {\n\t\tcase zerolog.DebugLevel:\n\t\t\tlogger.Debug().Ctx(ctx).Msg(message)\n\t\tcase zerolog.InfoLevel:\n\t\t\tlogger.Info().Ctx(ctx).Msg(message)\n\t\tcase zerolog.WarnLevel:\n\t\t\tlogger.Warn().Ctx(ctx).Msg(message)\n\t\tcase zerolog.ErrorLevel:\n\t\t\tlogger.Error().Ctx(ctx).Msg(message)\n\t\tcase zerolog.FatalLevel:\n\t\t\tlogger.Fatal().Ctx(ctx).Msg(message)\n\t\tcase zerolog.PanicLevel:\n\t\t\tlogger.Panic().Ctx(ctx).Msg(message)\n\t\tcase zerolog.TraceLevel:\n\t\t\tlogger.Trace().Ctx(ctx).Msg(message)\n\t\t}\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "v3/zerolog/zerolog_test.go",
    "content": "package zerolog\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\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/stretchr/testify/assert\"\n\n\t\"github.com/gofiber/fiber/v3\"\n\t\"github.com/gofiber/fiber/v3/middleware/requestid\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Test_GetResBody(t *testing.T) {\n\tt.Parallel()\n\n\treadableResBody := \"this is readable response body\"\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tGetResBody: func(c fiber.Ctx) []byte {\n\t\t\treturn []byte(readableResBody)\n\t\t},\n\t\tFields: []string{FieldResBody},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"------this is unreadable resp------\")\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, readableResBody, logs[FieldResBody])\n}\n\nfunc Test_SkipBody(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSkipField: func(field string, _ fiber.Ctx) bool {\n\t\t\treturn field == FieldBody\n\t\t},\n\t\tLogger: &logger,\n\t\tFields: []string{FieldPID, FieldBody},\n\t}))\n\n\tbody := bytes.NewReader([]byte(\"this is test\"))\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", body))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\t_, ok := logs[FieldBody]\n\n\tassert.Equal(t, false, ok)\n}\n\nfunc Test_SkipResBody(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tSkipField: func(field string, _ fiber.Ctx) bool {\n\t\t\treturn field == FieldResBody\n\t\t},\n\t\tLogger: &logger,\n\t\tFields: []string{FieldResBody},\n\t}))\n\n\tbody := bytes.NewReader([]byte(\"this is test\"))\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", body))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\t_, ok := logs[FieldResBody]\n\n\tassert.Equal(t, false, ok)\n}\n\nfunc Test_Logger(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn errors.New(\"some random error\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, \"some random error\", logs[FieldError])\n\tassert.Equal(t, float64(500), logs[FieldStatus])\n}\n\nfunc Test_Latency(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn c.SendStatus(fiber.StatusOK)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tlatencyStr, ok := logs[FieldLatency].(string)\n\tassert.Equal(t, true, ok)\n\tassert.Equal(t, true, strings.Contains(latencyStr, \"ms\"))\n\tassert.Equal(t, float64(200), logs[FieldStatus])\n}\n\nfunc Test_Logger_Next(t *testing.T) {\n\tt.Parallel()\n\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(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n}\n\nfunc Test_Logger_All(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{\n\t\t\tFieldProtocol,\n\t\t\tFieldPID,\n\t\t\tFieldBody,\n\t\t\tFieldIP,\n\t\t\tFieldHost,\n\t\t\tFieldURL,\n\t\t\tFieldLatency,\n\t\t\tFieldRoute,\n\t\t\tFieldMethod,\n\t\t\tFieldResBody,\n\t\t\tFieldQueryParams,\n\t\t\tFieldBytesReceived,\n\t\t\tFieldBytesSent,\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\treturn c.SendStatus(fiber.StatusNotFound)\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/?foo=bar\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusNotFound, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"body\":          \"\",\n\t\t\"ip\":            \"0.0.0.0\",\n\t\t\"host\":          \"example.com\",\n\t\t\"url\":           \"/?foo=bar\",\n\t\t\"level\":         \"warn\",\n\t\t\"message\":       \"Client error\",\n\t\t\"method\":        \"GET\",\n\t\t\"route\":         \"/\",\n\t\t\"protocol\":      \"HTTP/1.1\",\n\t\t\"pid\":           float64(os.Getpid()),\n\t\t\"queryParams\":   \"foo=bar\",\n\t\t\"resBody\":       \"Not Found\",\n\t\t\"bytesReceived\": float64(0),\n\t\t\"bytesSent\":     float64(9),\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tfor key, value := range expected {\n\t\tassert.Equal(t, value, logs[key])\n\t}\n\n\tlatencyStr, ok := logs[FieldLatency].(string)\n\tassert.Equal(t, true, ok)\n\tassert.Equal(t, true, strings.Contains(latencyStr, \"ms\"))\n}\n\nfunc Test_Response_Body(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldResBody},\n\t}))\n\n\texpectedGetResponse := \"Sample response body\"\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(expectedGetResponse)\n\t})\n\n\t_, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expectedGetResponse, logs[FieldResBody])\n}\n\nfunc Test_Request_Id(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldRequestID},\n\t}))\n\n\trequestID := \"bf985e8e-6a32-42ec-8e50-05a21db8f0e4\"\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Response().Header.Set(fiber.HeaderXRequestID, requestID)\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, requestID, logs[FieldRequestID])\n}\n\nfunc Test_Skip_URIs(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tNext: func(c fiber.Ctx) bool {\n\t\t\treturn c.Path() == \"/ignore_logging\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/ignore_logging\", func(c fiber.Ctx) error {\n\t\treturn errors.New(\"no log\")\n\t})\n\n\tresp, err := app.Test(httptest.NewRequest(\"GET\", \"/ignore_logging\", nil))\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)\n\tassert.Equal(t, 0, buf.Len())\n}\n\nfunc Test_Req_Headers(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldReqHeaders},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\treq.Header.Add(\"foo\", \"bar\")\n\treq.Header.Add(\"baz\", \"foo\")\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"Host\":    \"example.com\",\n\t\t\"Baz\":     \"foo\",\n\t\t\"Foo\":     \"bar\",\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_Req_Headers_WrapHeaders(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger:      &logger,\n\t\tFields:      []string{FieldReqHeaders},\n\t\tWrapHeaders: true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\treq.Header.Add(\"foo\", \"bar\")\n\treq.Header.Add(\"baz\", \"foo\")\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"reqHeaders\": map[string]interface{}{\n\t\t\t\"Host\": \"example.com\",\n\t\t\t\"Baz\":  \"foo\",\n\t\t\t\"Foo\":  \"bar\",\n\t\t},\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_Res_Headers(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldResHeaders},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(\"foo\", \"bar\")\n\t\tc.Set(\"baz\", \"foo\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"Content-Type\": \"text/plain; charset=utf-8\",\n\t\t\"Baz\":          \"foo\",\n\t\t\"Foo\":          \"bar\",\n\t\t\"level\":        \"info\",\n\t\t\"message\":      \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_Res_Headers_WrapHeaders(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger:      &logger,\n\t\tFields:      []string{FieldResHeaders},\n\t\tWrapHeaders: true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(\"foo\", \"bar\")\n\t\tc.Set(\"baz\", \"foo\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"resHeaders\": map[string]interface{}{\n\t\t\t\"Content-Type\": \"text/plain; charset=utf-8\",\n\t\t\t\"Baz\":          \"foo\",\n\t\t\t\"Foo\":          \"bar\",\n\t\t},\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_FieldsSnakeCase(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(requestid.New())\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{\n\t\t\tFieldResBody,\n\t\t\tFieldQueryParams,\n\t\t\tFieldBytesReceived,\n\t\t\tFieldBytesSent,\n\t\t\tFieldRequestID,\n\t\t\tFieldResHeaders,\n\t\t\tFieldReqHeaders,\n\t\t},\n\t\tFieldsSnakeCase: true,\n\t\tWrapHeaders:     true,\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(\"Foo\", \"bar\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/?param=value\", nil)\n\treq.Header.Add(\"X-Request-ID\", \"uuid\")\n\treq.Header.Add(\"Baz\", \"foo\")\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"bytes_received\": float64(0),\n\t\t\"bytes_sent\":     float64(5),\n\t\t\"query_params\":   \"param=value\",\n\t\t\"req_headers\": map[string]interface{}{\n\t\t\t\"Host\":         \"example.com\",\n\t\t\t\"Baz\":          \"foo\",\n\t\t\t\"X-Request-Id\": \"uuid\",\n\t\t},\n\t\t\"res_headers\": map[string]interface{}{\n\t\t\t\"Content-Type\": \"text/plain; charset=utf-8\",\n\t\t\t\"Foo\":          \"bar\",\n\t\t\t\"X-Request-Id\": \"uuid\",\n\t\t},\n\t\t\"request_id\": \"uuid\",\n\t\t\"res_body\":   \"hello\",\n\t\t\"level\":      \"info\",\n\t\t\"message\":    \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_LoggerLevelsAndMessages(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tlevels := []zerolog.Level{zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel}\n\tmessages := []string{\"server error\", \"client error\", \"success\"}\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger:   &logger,\n\t\tMessages: messages,\n\t\tLevels:   levels,\n\t}))\n\n\tapp.Get(\"/200\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusOK)\n\t\treturn nil\n\t})\n\tapp.Get(\"/400\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusBadRequest)\n\t\treturn nil\n\t})\n\tapp.Get(\"/500\", func(c fiber.Ctx) error {\n\t\tc.Status(fiber.StatusInternalServerError)\n\t\treturn nil\n\t})\n\n\ttests := []struct {\n\t\tReq     *http.Request\n\t\tStatus  int\n\t\tLevel   string\n\t\tMessage string\n\t}{\n\t\t{\n\t\t\tReq:     httptest.NewRequest(\"GET\", \"/500\", nil),\n\t\t\tStatus:  fiber.StatusInternalServerError,\n\t\t\tLevel:   levels[0].String(),\n\t\t\tMessage: messages[0],\n\t\t},\n\t\t{\n\t\t\tReq:     httptest.NewRequest(\"GET\", \"/400\", nil),\n\t\t\tStatus:  fiber.StatusBadRequest,\n\t\t\tLevel:   levels[1].String(),\n\t\t\tMessage: messages[1],\n\t\t},\n\t\t{\n\t\t\tReq:     httptest.NewRequest(\"GET\", \"/200\", nil),\n\t\t\tStatus:  fiber.StatusOK,\n\t\t\tLevel:   levels[2].String(),\n\t\t\tMessage: messages[2],\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tname := fmt.Sprintf(\"%s %s\", test.Req.Method, test.Req.URL)\n\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tbuf.Reset()\n\t\t\tresp, err := app.Test(test.Req)\n\n\t\t\tassert.Equal(t, nil, err)\n\t\t\tassert.Equal(t, test.Status, resp.StatusCode)\n\n\t\t\tvar logs map[string]any\n\t\t\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\t\t\tassert.Equal(t, test.Level, logs[\"level\"])\n\t\t\tassert.Equal(t, test.Message, logs[\"message\"])\n\t\t})\n\t}\n}\n\nfunc Test_Logger_FromContext(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tGetLogger: func(c fiber.Ctx) zerolog.Logger {\n\t\t\treturn zerolog.New(&buf).\n\t\t\t\tWith().\n\t\t\t\tStr(\"foo\", \"bar\").\n\t\t\t\tLogger()\n\t\t},\n\t}))\n\n\t_, err := app.Test(httptest.NewRequest(\"GET\", \"/\", nil))\n\tassert.Equal(t, nil, err)\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, \"bar\", logs[\"foo\"])\n}\n\nfunc Test_Logger_WhitelistHeaders(t *testing.T) {\n\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldReqHeaders},\n\t\tSkipHeader: func(header string, _ fiber.Ctx) bool {\n\t\t\tswitch header {\n\t\t\tcase \"Foo\", \"Host\", \"Bar\":\n\t\t\t\treturn false\n\t\t\tdefault:\n\t\t\t\treturn true\n\t\t\t}\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\treq.Header.Add(\"foo\", \"bar\")\n\treq.Header.Add(\"baz\", \"foo\")\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"Host\":    \"example.com\",\n\t\t\"Foo\":     \"bar\",\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n\n\tapp.Get(\"/res-headers\", func(c fiber.Ctx) error {\n\t\tc.Set(\"test\", \"skip\")\n\t\tc.Set(\"bar\", \"bar\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\treq = httptest.NewRequest(\"GET\", \"/res-headers\", nil)\n\treq.Header.Add(\"foo\", \"bar\")\n\treq.Header.Add(\"baz\", \"foo\")\n\n\tresp, err = app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected = map[string]interface{}{\n\t\t\"Bar\":     \"bar\",\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n}\n\nfunc Test_WhitelistHeaders_Resp_Headers(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldResHeaders},\n\t\tSkipHeader: func(header string, _ fiber.Ctx) bool {\n\t\t\treturn header != \"Bar\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(\"test\", \"skip\")\n\t\tc.Set(\"bar\", \"bar\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"Bar\":     \"bar\",\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_Logger_BlacklistHeaders(t *testing.T) {\n\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldReqHeaders},\n\t\tSkipHeader: func(header string, _ fiber.Ctx) bool {\n\t\t\treturn header == \"Foo\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\treturn c.SendString(\"hello\")\n\t})\n\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\treq.Header.Add(\"foo\", \"bar\")\n\treq.Header.Add(\"baz\", \"foo\")\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"Host\":    \"example.com\",\n\t\t\"Baz\":     \"foo\",\n\t\t\"level\":   \"info\",\n\t\t\"message\": \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n\nfunc Test_BlacklistHeaders_Resp_Headers(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf bytes.Buffer\n\tlogger := zerolog.New(&buf)\n\n\tapp := fiber.New()\n\tapp.Use(New(Config{\n\t\tLogger: &logger,\n\t\tFields: []string{FieldResHeaders},\n\t\tSkipHeader: func(header string, _ fiber.Ctx) bool {\n\t\t\treturn header == \"Test\"\n\t\t},\n\t}))\n\n\tapp.Get(\"/\", func(c fiber.Ctx) error {\n\t\tc.Set(\"test\", \"skip\")\n\t\tc.Set(\"bar\", \"bar\")\n\t\treturn c.SendString(\"hello\")\n\t})\n\treq := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tresp, err := app.Test(req)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, fiber.StatusOK, resp.StatusCode)\n\n\texpected := map[string]interface{}{\n\t\t\"Bar\":          \"bar\",\n\t\t\"Content-Type\": \"text/plain; charset=utf-8\",\n\t\t\"level\":        \"info\",\n\t\t\"message\":      \"Success\",\n\t}\n\n\tvar logs map[string]any\n\t_ = json.Unmarshal(buf.Bytes(), &logs)\n\n\tassert.Equal(t, expected, logs)\n}\n"
  }
]