Repository: gofiber/contrib Branch: main Commit: 09779e8bbddf Files: 200 Total size: 900.7 KB Directory structure: gitextract_6_bal560/ ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yaml │ │ ├── feature-request.yaml │ │ └── question.yaml │ ├── dependabot.yml │ ├── release-plan.yml │ ├── release.yml │ ├── scripts/ │ │ └── parallel-go-test.sh │ └── workflows/ │ ├── after-release.yml │ ├── auto-labeler.yml │ ├── cleanup-release-draft.yml │ ├── dependabot-on-demand.yml │ ├── dependabot_automerge.yml │ ├── lint.yml │ ├── release-drafter.yml │ ├── sync-docs.yml │ ├── test-casbin.yml │ ├── test-circuitbreaker.yml │ ├── test-coraza.yml │ ├── test-fgprof.yml │ ├── test-hcaptcha.yml │ ├── test-i18n.yml │ ├── test-jwt.yml │ ├── test-loadshed.yml │ ├── test-monitor.yml │ ├── test-newrelic.yml │ ├── test-opa.yml │ ├── test-otel.yml │ ├── test-paseto.yml │ ├── test-sentry.yml │ ├── test-socketio.yml │ ├── test-swaggerui.yml │ ├── test-swaggo.yml │ ├── test-testcontainers.yml │ ├── test-websocket.yml │ ├── test-zap.yml │ ├── test-zerolog.yml │ └── weekly-release.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.work └── v3/ ├── .golangci.yml ├── README.md ├── casbin/ │ ├── README.md │ ├── casbin.go │ ├── casbin_test.go │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── options.go │ └── utils.go ├── circuitbreaker/ │ ├── README.md │ ├── circuitbreaker.go │ ├── circuitbreaker_test.go │ ├── go.mod │ └── go.sum ├── coraza/ │ ├── README.md │ ├── coraza.go │ ├── coraza_test.go │ ├── go.mod │ ├── go.sum │ └── metrics.go ├── fgprof/ │ ├── README.md │ ├── config.go │ ├── fgprof.go │ ├── fgprof_test.go │ ├── go.mod │ └── go.sum ├── hcaptcha/ │ ├── README.md │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── hcaptcha.go │ └── hcaptcha_test.go ├── i18n/ │ ├── README.md │ ├── config.go │ ├── embed.go │ ├── embed_test.go │ ├── example/ │ │ ├── localize/ │ │ │ ├── en.yaml │ │ │ └── zh.yaml │ │ ├── localizeJSON/ │ │ │ ├── en.json │ │ │ └── zh.json │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── i18n.go │ └── i18n_test.go ├── jwt/ │ ├── README.md │ ├── config.go │ ├── config_test.go │ ├── crypto.go │ ├── go.mod │ ├── go.sum │ ├── jwt.go │ └── jwt_test.go ├── loadshed/ │ ├── README.md │ ├── cpu.go │ ├── go.mod │ ├── go.sum │ ├── loadshed.go │ └── loadshed_test.go ├── monitor/ │ ├── README.md │ ├── config.go │ ├── config_test.go │ ├── go.mod │ ├── go.sum │ ├── index.go │ ├── monitor.go │ └── monitor_test.go ├── newrelic/ │ ├── README.md │ ├── fiber.go │ ├── fiber_test.go │ ├── go.mod │ └── go.sum ├── opa/ │ ├── README.md │ ├── fiber.go │ ├── fiber_test.go │ ├── go.mod │ └── go.sum ├── otel/ │ ├── README.md │ ├── config.go │ ├── doc.go │ ├── example/ │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── docker-compose.yml │ │ ├── go.mod │ │ ├── go.sum │ │ └── server.go │ ├── fiber.go │ ├── fiber_context_test.go │ ├── go.mod │ ├── go.sum │ ├── internal/ │ │ ├── http.go │ │ └── http_test.go │ ├── otel_test/ │ │ └── fiber_test.go │ └── semconv.go ├── paseto/ │ ├── README.md │ ├── config.go │ ├── config_test.go │ ├── go.mod │ ├── go.sum │ ├── helpers.go │ ├── paseto.go │ ├── paseto_test.go │ └── payload.go ├── sentry/ │ ├── README.md │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── sentry.go │ └── sentry_test.go ├── socketio/ │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── socketio.go │ └── socketio_test.go ├── swaggerui/ │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── swagger.go │ ├── swagger.json │ ├── swagger.yaml │ ├── swagger_missing.json │ └── swagger_test.go ├── swaggo/ │ ├── README.md │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── index.go │ ├── swagger.go │ └── swagger_test.go ├── testcontainers/ │ ├── README.md │ ├── config.go │ ├── examples_test.go │ ├── go.mod │ ├── go.sum │ ├── testcontainers.go │ ├── testcontainers_test.go │ └── testcontainers_unit_test.go ├── websocket/ │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── websocket.go │ └── websocket_test.go ├── zap/ │ ├── .gitignore │ ├── README.md │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── logger.go │ ├── logger_test.go │ ├── zap.go │ └── zap_test.go └── zerolog/ ├── README.md ├── config.go ├── go.mod ├── go.sum ├── zerolog.go └── zerolog_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ * @gofiber/maintainers ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yaml ================================================ name: "\U0001F41B Bug Report" title: "\U0001F41B [Bug]: " description: Create a bug report to help us fix it. labels: ["☢️ Bug"] body: - type: markdown id: notice attributes: value: | ### Notice - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord). - If you think Fiber contrib don't have a nice feature that you think, open the issue with **✏️ Feature Request** template. - Write your issue with clear and understandable English. - type: textarea id: description attributes: label: "Bug Description" description: "A clear and detailed description of what the bug is." placeholder: "Explain your problem as clear and detailed." validations: required: true - type: textarea id: how-to-reproduce attributes: label: How to Reproduce description: "Steps to reproduce the behavior and what should be observed in the end." placeholder: "Tell us step by step how we can replicate your problem and what we should see in the end." value: | Steps to reproduce the behavior: 1. Go to '....' 2. Click on '....' 3. Do '....' 4. See '....' validations: required: true - type: textarea id: expected-behavior attributes: label: Expected Behavior description: "A clear and detailed description of what you think should happens." placeholder: "Tell us what contrib should normally do." validations: required: true - type: input id: version attributes: label: "Contrib package Version" description: "Some bugs may be fixed in future contrib releases, so we have to know your contrib package version." placeholder: "Write your contrib version. (v1.0.0, v1.1.0...)" validations: required: true - type: textarea id: snippet attributes: label: "Code Snippet (optional)" description: "For some issues, we need to know some parts of your code." placeholder: "Share a code you think related to the issue." render: go value: | package main // Replace with the module's current major version (omit the `/v` suffix entirely for v1 modules). import "github.com/gofiber/contrib/v3/%package%" func main() { // Steps to reproduce } - type: checkboxes id: terms attributes: label: "Checklist:" description: "By submitting this issue, you confirm that:" options: - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." required: true - label: "I have checked for existing issues that describe my problem prior to opening this one." required: true - label: "I understand that improperly formatted bug reports may be closed without explanation." required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yaml ================================================ name: "\U0001F680 Feature Request" title: "\U0001F680 [Feature]: " description: Suggest an idea to improve this project. labels: ["✏️ Feature"] body: - type: markdown id: notice attributes: value: | ### Notice - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord). - If you think this is just a bug, open the issue with **☢️ Bug Report** template. - Write your issue with clear and understandable English. - type: textarea id: description attributes: label: "Feature Description" description: "A clear and detailed description of the feature we need to do." placeholder: "Explain your feature as clear and detailed." validations: required: true - type: textarea id: additional-context attributes: label: "Additional Context (optional)" description: "If you have something else to describe, write them here." placeholder: "Write here what you can describe differently." - type: textarea id: snippet attributes: label: "Code Snippet (optional)" description: "Code snippet may be really helpful to describe some features." placeholder: "Share a code to explain the feature better." render: go value: | package main // Replace with the module's current major version (omit the `/v` suffix entirely for v1 modules). import "github.com/gofiber/contrib/v3/%package%" func main() { // Steps to reproduce } - type: checkboxes id: terms attributes: label: "Checklist:" description: "By submitting this issue, you confirm that:" options: - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." required: true - label: "I have checked for existing issues that describe my suggestion prior to opening this one." required: true - label: "I understand that improperly formatted feature requests may be closed without explanation." required: true ================================================ FILE: .github/ISSUE_TEMPLATE/question.yaml ================================================ name: "🤔 Question" title: "\U0001F917 [Question]: " description: Ask a question so we can help you easily. labels: ["🤔 Question"] body: - type: markdown id: notice attributes: value: | ### Notice - Dont't forget you can ask your questions on our [Discord server](https://gofiber.io/discord). - If you think this is just a bug, open the issue with **☢️ Bug Report** template. - If you think Fiber contrib don't have a nice feature that you think, open the issue with **✏️ Feature Request** template. - Write your issue with clear and understandable English. - type: textarea id: description attributes: label: "Question Description" description: "A clear and detailed description of the question." placeholder: "Explain your question as clear and detailed." validations: required: true - type: textarea id: snippet attributes: label: "Code Snippet (optional)" description: "Code snippet may be really helpful to describe some features." placeholder: "Share a code to explain the feature better." render: go value: | package main // Replace with the module's current major version (omit the `/v` suffix entirely for v1 modules). import "github.com/gofiber/contrib/v3/%package%" func main() { // Steps to reproduce } - type: checkboxes id: terms attributes: label: "Checklist:" description: "By submitting this issue, you confirm that:" options: - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." required: true - label: "I have checked for existing issues that describe my questions prior to opening this one." required: true - label: "I understand that improperly formatted questions may be closed without explanation." required: true ================================================ FILE: .github/dependabot.yml ================================================ # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#directories version: 2 updates: - package-ecosystem: "github-actions" open-pull-requests-limit: 50 directory: "/" labels: - "🤖 Dependencies" schedule: interval: "daily" # gomod split by alphabet ranges to reduce group PR size # a-l ~8 modules | m-r ~6 modules | s-z ~8 modules - package-ecosystem: "gomod" open-pull-requests-limit: 20 allow: - dependency-type: "all" directories: - "/v3/a*" - "/v3/b*" - "/v3/c*" - "/v3/d*" - "/v3/e*" - "/v3/f*" - "/v3/g*" - "/v3/h*" - "/v3/i*" - "/v3/j*" - "/v3/k*" - "/v3/l*" labels: - "🤖 Dependencies" schedule: interval: "daily" groups: fiber-modules: patterns: - "github.com/gofiber/fiber/**" fiber-utils-modules: patterns: - "github.com/gofiber/utils" - "github.com/gofiber/utils/**" fiber-schema-modules: patterns: - "github.com/gofiber/schema" - "github.com/gofiber/schema/**" fasthttp-modules: patterns: - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/**" golang-modules: patterns: - "golang.org/x/**" opentelemetry-modules: patterns: - "go.opentelemetry.io/**" fasthttp-websocket-modules: patterns: - "github.com/fasthttp/websocket/**" valyala-utils-modules: patterns: - "github.com/valyala/bytebufferpool" - "github.com/valyala/tcplisten" mattn-modules: patterns: - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" - "github.com/mattn/go-runewidth" shirou-modules: patterns: - "github.com/shirou/gopsutil" - "github.com/shirou/gopsutil/**" ebitengine-modules: patterns: - "github.com/ebitengine/**" klauspost-modules: patterns: - "github.com/klauspost/**" tklauser-modules: patterns: - "github.com/tklauser/**" google-modules: patterns: - "github.com/google/**" - "google.golang.org/**" testing-modules: patterns: - "github.com/stretchr/**" - "github.com/davecgh/go-spew" - "github.com/pmezard/go-difflib" check-testing-modules: patterns: - "gopkg.in/check.v*" - "github.com/kr/**" - "github.com/rogpeppe/go-internal" tinylib-modules: patterns: - "github.com/tinylib/**" msgp-modules: patterns: - "github.com/philhofer/fwd" openapi-modules: patterns: - "github.com/go-openapi/**" yaml-modules: patterns: - "gopkg.in/yaml.*" - "go.yaml.in/yaml/**" - "sigs.k8s.io/yaml" andybalholm-modules: patterns: - "github.com/andybalholm/**" - package-ecosystem: "gomod" open-pull-requests-limit: 20 allow: - dependency-type: "all" directories: - "/v3/m*" - "/v3/n*" - "/v3/o*" - "/v3/o*/*" - "/v3/p*" - "/v3/q*" - "/v3/r*" labels: - "🤖 Dependencies" schedule: interval: "daily" groups: fiber-modules: patterns: - "github.com/gofiber/fiber/**" fiber-utils-modules: patterns: - "github.com/gofiber/utils" - "github.com/gofiber/utils/**" fiber-schema-modules: patterns: - "github.com/gofiber/schema" - "github.com/gofiber/schema/**" fasthttp-modules: patterns: - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/**" golang-modules: patterns: - "golang.org/x/**" opentelemetry-modules: patterns: - "go.opentelemetry.io/**" fasthttp-websocket-modules: patterns: - "github.com/fasthttp/websocket/**" valyala-utils-modules: patterns: - "github.com/valyala/bytebufferpool" - "github.com/valyala/tcplisten" mattn-modules: patterns: - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" - "github.com/mattn/go-runewidth" shirou-modules: patterns: - "github.com/shirou/gopsutil" - "github.com/shirou/gopsutil/**" ebitengine-modules: patterns: - "github.com/ebitengine/**" klauspost-modules: patterns: - "github.com/klauspost/**" tklauser-modules: patterns: - "github.com/tklauser/**" google-modules: patterns: - "github.com/google/**" - "google.golang.org/**" testing-modules: patterns: - "github.com/stretchr/**" - "github.com/davecgh/go-spew" - "github.com/pmezard/go-difflib" check-testing-modules: patterns: - "gopkg.in/check.v*" - "github.com/kr/**" - "github.com/rogpeppe/go-internal" tinylib-modules: patterns: - "github.com/tinylib/**" msgp-modules: patterns: - "github.com/philhofer/fwd" openapi-modules: patterns: - "github.com/go-openapi/**" yaml-modules: patterns: - "gopkg.in/yaml.*" - "go.yaml.in/yaml/**" - "sigs.k8s.io/yaml" andybalholm-modules: patterns: - "github.com/andybalholm/**" - package-ecosystem: "gomod" open-pull-requests-limit: 20 allow: - dependency-type: "all" directories: - "/v3/s*" - "/v3/t*" - "/v3/u*" - "/v3/v*" - "/v3/w*" - "/v3/x*" - "/v3/y*" - "/v3/z*" labels: - "🤖 Dependencies" schedule: interval: "daily" groups: fiber-modules: patterns: - "github.com/gofiber/fiber/**" fiber-utils-modules: patterns: - "github.com/gofiber/utils" - "github.com/gofiber/utils/**" fiber-schema-modules: patterns: - "github.com/gofiber/schema" - "github.com/gofiber/schema/**" fasthttp-modules: patterns: - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/**" golang-modules: patterns: - "golang.org/x/**" opentelemetry-modules: patterns: - "go.opentelemetry.io/**" fasthttp-websocket-modules: patterns: - "github.com/fasthttp/websocket/**" valyala-utils-modules: patterns: - "github.com/valyala/bytebufferpool" - "github.com/valyala/tcplisten" mattn-modules: patterns: - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" - "github.com/mattn/go-runewidth" shirou-modules: patterns: - "github.com/shirou/gopsutil" - "github.com/shirou/gopsutil/**" ebitengine-modules: patterns: - "github.com/ebitengine/**" klauspost-modules: patterns: - "github.com/klauspost/**" tklauser-modules: patterns: - "github.com/tklauser/**" google-modules: patterns: - "github.com/google/**" - "google.golang.org/**" testing-modules: patterns: - "github.com/stretchr/**" - "github.com/davecgh/go-spew" - "github.com/pmezard/go-difflib" check-testing-modules: patterns: - "gopkg.in/check.v*" - "github.com/kr/**" - "github.com/rogpeppe/go-internal" tinylib-modules: patterns: - "github.com/tinylib/**" msgp-modules: patterns: - "github.com/philhofer/fwd" openapi-modules: patterns: - "github.com/go-openapi/**" yaml-modules: patterns: - "gopkg.in/yaml.*" - "go.yaml.in/yaml/**" - "sigs.k8s.io/yaml" andybalholm-modules: patterns: - "github.com/andybalholm/**" ================================================ FILE: .github/release-plan.yml ================================================ # Release plan for gofiber/contrib. # # Only modules with ordering constraints need explicit waves. # The final wave with `auto-discover: true` picks up all remaining # draft releases that weren't handled by earlier waves. # # Constraint: socketio depends on websocket (local replace). # Websocket's post-release hook triggers dependabot in this repo # immediately so socketio picks up the new version before wave 2. # # After all modules are published, 'after-release' is dispatched once. # The after-release.yml workflow defines which repos to notify. wave-delay-minutes: 30 waves: - name: base modules: - name: websocket tag-prefix: "v3/websocket/" post-release: - action: dispatch repo: gofiber/contrib event: dependabot-on-demand - name: remaining auto-discover: true post-release: - action: dispatch event: after-release - action: dispatch event: sync-docs ================================================ FILE: .github/release.yml ================================================ # .github/release.yml changelog: categories: - title: '❗ Breaking Changes' labels: - '❗ BreakingChange' - title: '🚀 New Features' labels: - '✏️ Feature' - '📝 Proposal' - title: '🧹 Updates' labels: - '🧹 Updates' - '⚡️ Performance' - title: '🐛 Bug Fixes' labels: - '☢️ Bug' - title: '🛠️ Maintenance' labels: - '🤖 Dependencies' - title: '📚 Documentation' labels: - '📒 Documentation' - title: 'Other Changes' labels: - '*' ================================================ FILE: .github/scripts/parallel-go-test.sh ================================================ #!/usr/bin/env bash # parallel-go-test.sh (refreshed) # Recursively find go.mod files under the current directory and run tests in each module. # - truncates tests-errors.log at start # - runs tests in parallel (jobs = CPU cores) # - prefers `gotestsum` when available; otherwise uses `go test` # - on failures, appends annotated error blocks (source line + one-line context) to tests-errors.log immediately # - filters out successful test noise (=== RUN / --- PASS / PASS / ok ...) # - atomic appends using mkdir-lock so parallel jobs don't interleave # - colored terminal output (OK = green, FAIL = red, WARN = yellow) # - handles Ctrl+C / SIGTERM: kills children and cleans up # - option: -m to run `go mod download` and `go mod vendor` in each module before tests set -euo pipefail IFS=$'\n\t' JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" LOGFILE="tests-errors.log" RUN_GOMOD=0 usage() { cat <<'USAGE' Usage: ./parallel-go-test.sh [-j JOBS] [-o LOGFILE] [-m] Options: -j JOBS Number of parallel jobs (defaults to CPU cores) -o LOGFILE Path to central logfile (defaults to tests-errors.log) -m Run `go mod download` and `go mod vendor` in each module before running tests Examples: ./parallel-go-test.sh # default behaviour ./parallel-go-test.sh -m # run `go mod download` + `go mod vendor` first ./parallel-go-test.sh -j 8 -m # 8 parallel jobs and run mod step USAGE } # parse options while getopts ":j:o:mh" opt; do case "$opt" in j) JOBS="$OPTARG" ;; o) LOGFILE="$OPTARG" ;; m) RUN_GOMOD=1 ;; h) usage; exit 0 ;; \?) printf 'Unknown option: -%s\n' "$OPTARG"; usage; exit 2 ;; :) printf 'Option -%s requires an argument.\n' "$OPTARG"; usage; exit 2 ;; esac done shift $((OPTIND -1)) # Colors (only if stdout is a tty) if [ -t 1 ]; then GREEN=$'\e[32m' RED=$'\e[31m' YELLOW=$'\e[33m' RESET=$'\e[0m' else GREEN="" RED="" YELLOW="" RESET="" fi # truncate central logfile at start : > "$LOGFILE" # detect gotestsum USE_GOTESTSUM=0 if command -v gotestsum >/dev/null 2>&1; then USE_GOTESTSUM=1 fi # find all module directories (skip common vendor trees) mapfile -t MODULE_DIRS < <( find . \( -path "./.git" -o -path "./vendor" -o -path "./node_modules" \) -prune -o \ -type f -name 'go.mod' -print0 | xargs -0 -n1 dirname | sort -u ) if [ "${#MODULE_DIRS[@]}" -eq 0 ]; then printf '%s\n' "No go.mod files found. Nothing to do." exit 0 fi # create absolute temp dir TMPDIR="$(mktemp -d 2>/dev/null || mktemp -d /tmp/tests-logs.XXXXXX)" if [ -z "$TMPDIR" ] || [ ! -d "$TMPDIR" ]; then printf '%s\n' "Failed to create temporary directory" >&2 exit 2 fi # cleanup routine cleanup_tmpdir() { local attempts=0 while [ "$attempts" -lt 5 ]; do if rm -rf "$TMPDIR" 2>/dev/null; then break fi attempts=$((attempts+1)) sleep 0.1 done if [ -d "$TMPDIR" ]; then printf '%s\n' "WARNING: could not fully remove temporary dir: $TMPDIR" fi } # signal handling: kill children, wait, cleanup, exit pids=() on_interrupt() { printf '%b\n' "${YELLOW}Received interrupt. Killing background jobs...${RESET}" for pid in "${pids[@]:-}"; do if kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true fi done sleep 0.1 for pid in "${pids[@]:-}"; do if kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true fi done cleanup_tmpdir printf '%b\n' "${YELLOW}Aborted by user.${RESET}" exit 130 } trap on_interrupt INT TERM # helper: sanitize a dir to a filename-safe token sanitize_name() { local d="$1" d="${d#./}" d="${d//\//__}" d="${d// /_}" printf '%s' "${d//[^A-Za-z0-9._-]/_}" } # annotate a module's temp log and append to central logfile atomically # This version: only appends when failure indicators are present and strips PASS/RUN noise annotate_and_append() { local src_log="$1" local module_dir="$2" local lockdir="$TMPDIR/.lock" # quick check: only append logs that contain failure indicators if ! grep -E -q '(--- FAIL:|^FAIL\b|panic:|exit status|FAIL\t|FAIL:)' "$src_log"; then # nothing to do (only PASS/ok output) rm -f "$src_log" >/dev/null 2>&1 || true return fi local annotated annotated="$(mktemp "$TMPDIR/annotated.XXXXXX")" || { until mkdir "$lockdir" 2>/dev/null; do sleep 0.01; done printf '==== %s ====\n' "$module_dir" >> "$LOGFILE" # filter out PASS/OK noise when falling back grep -v -E '^(=== RUN|--- PASS:|^PASS$|^ok\s)' "$src_log" >> "$LOGFILE" || true rm -f "$src_log" || true rmdir "$lockdir" 2>/dev/null || true return } # process lines in src_log, skipping PASS/RUN lines and annotating file:line occurrences while IFS= read -r line || [ -n "$line" ]; do # skip successful-test noise if [[ $line =~ ^(===\ RUN|---\ PASS:|^PASS$|^ok\s) ]]; then continue fi # match paths like path/to/file.go:LINE or file.go:LINE:COL if [[ $line =~ ^([^:]+\.go):([0-9]+):?([0-9]*)[:[:space:]]*(.*)$ ]]; then local fp="${BASH_REMATCH[1]}" local ln="${BASH_REMATCH[2]}" local col="${BASH_REMATCH[3]}" local rest="${BASH_REMATCH[4]}" local candidate="" if [ -f "$fp" ]; then candidate="$fp" elif [ -f "$module_dir/$fp" ]; then candidate="$module_dir/$fp" elif [ -f "./$fp" ]; then candidate="./$fp" fi if [ -n "$candidate" ]; then local start=$(( ln > 1 ? ln - 1 : 1 )) local end=$(( ln + 1 )) printf '%s\n' "---- source: $candidate:$ln ----" >> "$annotated" awk -v s="$start" -v e="$end" 'NR>=s && NR<=e { printf("%6d %s\n", NR, $0) }' "$candidate" >> "$annotated" printf '%s\n\n' "Error: $line" >> "$annotated" else printf '%s\n' "---- (source not found) $line ----" >> "$annotated" fi else printf '%s\n' "$line" >> "$annotated" fi done < "$src_log" # append atomically under lock until mkdir "$lockdir" 2>/dev/null; do sleep 0.01 done { printf '==== %s ====\n' "$module_dir" cat "$annotated" printf '\n\n' } >> "$LOGFILE" rm -f "$annotated" "$src_log" || true rmdir "$lockdir" 2>/dev/null || true } # run tests for one module (with optional go mod download + vendor step) run_tests() { local module_dir="$1" local safe safe="$(sanitize_name "$module_dir")" local mod_log="$TMPDIR/$safe.log" mkdir -p "$(dirname "$mod_log")" # optionally run `go mod download` and `go mod vendor` first if [ "$RUN_GOMOD" -eq 1 ]; then local mod_step_log="$TMPDIR/$safe.modlog" : > "$mod_step_log" ( cd "$module_dir" 2>/dev/null && go mod download >>"$mod_step_log" 2>&1 ) || true ( cd "$module_dir" 2>/dev/null && go mod vendor >>"$mod_step_log" 2>&1 ) || true if [ -s "$mod_step_log" ]; then printf '%b\n' "${YELLOW}MOD: ${RESET}$module_dir (go mod output appended)" annotate_and_append "$mod_step_log" "$module_dir" else rm -f "$mod_step_log" >/dev/null 2>&1 || true fi fi # run tests: always capture stdout+stderr to per-module log so nothing prints on success if [ "$USE_GOTESTSUM" -eq 1 ]; then # use gotestsum but still capture its output to file if ( cd "$module_dir" 2>/dev/null && gotestsum --format standard-verbose -- -test.v ./... >"$mod_log" 2>&1 ); then printf '%b\n' "${GREEN}OK: ${RESET}$module_dir" rm -f "$mod_log" >/dev/null 2>&1 || true return 0 else printf '%b\n' "${RED}FAIL: ${RESET}$module_dir (appending to $LOGFILE)" annotate_and_append "$mod_log" "$module_dir" return 1 fi else # fallback to go test; capture everything if ( cd "$module_dir" 2>/dev/null && go test ./... -run "" -v >"$mod_log" 2>&1 ); then printf '%b\n' "${GREEN}OK: ${RESET}$module_dir" rm -f "$mod_log" >/dev/null 2>&1 || true return 0 else printf '%b\n' "${RED}FAIL: ${RESET}$module_dir (appending to $LOGFILE)" annotate_and_append "$mod_log" "$module_dir" return 1 fi fi } # main launcher: spawn jobs, throttle to JOBS, wait properly fail_count=0 for md in "${MODULE_DIRS[@]}"; do run_tests "$md" & pids+=( "$!" ) if [ "${#pids[@]}" -ge "$JOBS" ]; then if wait "${pids[0]}"; then : else fail_count=$((fail_count+1)) fi pids=( "${pids[@]:1}" ) fi done # wait remaining background jobs for pid in "${pids[@]:-}"; do if wait "$pid"; then :; else fail_count=$((fail_count+1)); fi done # final cleanup and status cleanup_tmpdir if [ "$fail_count" -gt 0 ]; then printf '\n%b\n' "${RED}Done. $fail_count module(s) failed. See $LOGFILE for details (annotated snippets included).${RESET}" exit 1 else printf '\n%b\n' "${GREEN}Done. All modules tested successfully.${RESET}" if [ -f "$LOGFILE" ] && [ ! -s "$LOGFILE" ]; then rm -f "$LOGFILE" fi exit 0 fi ================================================ FILE: .github/workflows/after-release.yml ================================================ name: After Release on: release: types: [published] repository_dispatch: types: [after-release] workflow_dispatch: jobs: # Websocket releases update socketio dep in same repo (manual releases only). # During weekly releases, the intra-wave hook in release-plan.yml handles this. self-update: if: >- github.event_name == 'release' && github.actor != 'github-actions[bot]' && contains(github.event.release.tag_name, 'websocket') uses: gofiber/.github/.github/workflows/after-release.yml@main with: skip-wait: ${{ github.event_name == 'workflow_dispatch' }} repos: | - gofiber/contrib secrets: dispatch-token: ${{ secrets.DISPATCH_TOKEN }} # Notify downstream repos. Runs for manual releases and weekly batch dispatch. notify-dependents: if: github.event_name != 'release' || github.actor != 'github-actions[bot]' uses: gofiber/.github/.github/workflows/after-release.yml@main with: skip-wait: ${{ github.event_name == 'workflow_dispatch' }} repos: | - gofiber/recipes secrets: dispatch-token: ${{ secrets.DISPATCH_TOKEN }} ================================================ FILE: .github/workflows/auto-labeler.yml ================================================ name: auto-labeler on: issues: types: [opened, edited, milestoned] pull_request_target: types: [opened, edited, reopened, synchronize] workflow_dispatch: jobs: auto-labeler: uses: gofiber/.github/.github/workflows/auto-labeler.yml@main secrets: github-token: ${{ secrets.ISSUE_PR_TOKEN }} with: config-path: .github/labeler.yml config-repository: gofiber/.github ================================================ FILE: .github/workflows/cleanup-release-draft.yml ================================================ name: Cleanup Release Draft on: workflow_dispatch: inputs: tag: description: 'Tag or name of the draft (empty = latest draft)' required: false type: string default: '' jobs: cleanup: runs-on: ubuntu-latest permissions: contents: write steps: - uses: gofiber/.github/.github/actions/cleanup-release-draft@main with: tag: ${{ inputs.tag }} ================================================ FILE: .github/workflows/dependabot-on-demand.yml ================================================ name: Dependabot On-Demand on: repository_dispatch: types: [trigger-dependabot] workflow_dispatch: jobs: trigger: uses: gofiber/.github/.github/workflows/dependabot-on-demand.yml@main secrets: push-token: ${{ secrets.PR_TOKEN }} ================================================ FILE: .github/workflows/dependabot_automerge.yml ================================================ name: Dependabot auto-merge on: workflow_dispatch: pull_request_target: permissions: contents: write pull-requests: write jobs: wait_for_checks: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Wait for check is finished id: wait_for_checks uses: poseidon/wait-for-status-checks@v0.6.0 with: token: ${{ secrets.PR_TOKEN }} match_pattern: Tests interval: 10 timeout: 600 dependabot: needs: [wait_for_checks] name: Dependabot auto-merge runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v3.1.0 with: github-token: "${{ secrets.PR_TOKEN }}" - name: Enable auto-merge for Dependabot PRs if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}} run: | gh pr review --approve "$PR_URL" gh pr merge --auto --merge "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.PR_TOKEN}} ================================================ FILE: .github/workflows/lint.yml ================================================ name: Linter on: push: branches: - master - main paths-ignore: - "**/*.md" - LICENSE - ".github/ISSUE_TEMPLATE/*.yml" - ".github/dependabot.yml" pull_request: paths-ignore: - "**/*.md" - LICENSE - ".github/ISSUE_TEMPLATE/*.yml" - ".github/dependabot.yml" workflow_dispatch: permissions: contents: read pull-requests: read checks: write jobs: lint: uses: gofiber/.github/.github/workflows/go-lint.yml@main with: repo-type: multi module-dir: v3 golangci-lint-args: "--tests=false --timeout=5m" ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter (v3 packages) on: push: branches: - master - main workflow_dispatch: permissions: contents: read jobs: changes: runs-on: ubuntu-latest permissions: contents: read pull-requests: read outputs: packages: ${{ github.event_name == 'workflow_dispatch' && steps.filter-setup.outputs.packages || steps.map-packages.outputs.packages || '[]' }} steps: - name: Checkout repository uses: actions/checkout@v6 - name: Generate filters id: filter-setup shell: bash run: | shopt -s nullglob packages=(v3/*/) package_names=() if (( ${#packages[@]} == 0 )); then echo "filters={}" >> "$GITHUB_OUTPUT" echo "packages=[]" >> "$GITHUB_OUTPUT" exit 0 fi { echo "filters<> "$GITHUB_OUTPUT" if (( ${#package_names[@]} > 0 )); then printf -v joined '"%s",' "${package_names[@]}" joined=${joined%,} echo "packages=[${joined}]" >> "$GITHUB_OUTPUT" else echo "packages=[]" >> "$GITHUB_OUTPUT" fi - name: Filter changes id: filter uses: dorny/paths-filter@v4 if: github.event_name != 'workflow_dispatch' with: filters: ${{ steps.filter-setup.outputs.filters }} - name: Map package paths id: map-packages if: github.event_name != 'workflow_dispatch' env: FILTER_PACKAGES: ${{ steps.filter.outputs.changes || '[]' }} run: | python3 - <<'PY' >> "$GITHUB_OUTPUT" import json import os packages = json.loads(os.environ["FILTER_PACKAGES"]) paths = [f"v3/{name}" for name in packages] print(f"packages={json.dumps(paths)}") PY release-drafter: needs: changes runs-on: ubuntu-latest timeout-minutes: 30 if: needs.changes.outputs.packages != '[]' permissions: contents: write pull-requests: read strategy: matrix: package: ${{ fromJSON(needs.changes.outputs.packages || '[]') }} steps: - name: Checkout shared config uses: actions/checkout@v6 with: repository: gofiber/.github sparse-checkout: .github/release-drafter-module.yml sparse-checkout-cone-mode: false - name: Generate config from shared template run: | sed "s|{{MODULE}}|${{ matrix.package }}|g" \ .github/release-drafter-module.yml > .github/release-drafter-parsed.yml - name: Run release drafter uses: release-drafter/release-drafter@v7 with: config-name: file:release-drafter-parsed.yml ================================================ FILE: .github/workflows/sync-docs.yml ================================================ name: Sync docs on: push: branches: [main, master] paths: ['**/*.md'] release: types: [published] repository_dispatch: types: [sync-docs] workflow_dispatch: inputs: mode: description: 'push = sync current docs, release-all = version snapshots for all latest module releases' type: choice options: [push, release-all] default: push jobs: sync: uses: gofiber/.github/.github/workflows/sync-docs.yml@main with: repo-type: multi source-dir: v3 destination-dir: docs/contrib version-file: contrib_versions.json docusaurus-command: npm run docusaurus -- docs:version:contrib commit-url: https://github.com/gofiber/contrib event-mode: >- ${{ github.event_name == 'workflow_dispatch' && inputs.mode || github.event_name == 'repository_dispatch' && 'release-all' || github.event_name }} tag-name: ${{ github.ref_name }} secrets: doc-sync-token: ${{ secrets.DOC_SYNC_TOKEN }} ================================================ FILE: .github/workflows/test-casbin.yml ================================================ name: "Test casbin" on: push: branches: - master - main paths: - 'v3/casbin/**/*.go' - 'v3/casbin/go.mod' pull_request: paths: - 'v3/casbin/**/*.go' - 'v3/casbin/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/casbin run: go test -v -race ./... ================================================ FILE: .github/workflows/test-circuitbreaker.yml ================================================ name: "Test CircuitBreaker" on: push: branches: - master - main paths: - 'v3/circuitbreaker/**/*.go' - 'v3/circuitbreaker/go.mod' pull_request: paths: - 'v3/circuitbreaker/**/*.go' - 'v3/circuitbreaker/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/circuitbreaker run: go test -v -race ./... ================================================ FILE: .github/workflows/test-coraza.yml ================================================ name: "Test Coraza" on: push: branches: - master - main paths: - 'v3/coraza/**/*.go' - 'v3/coraza/go.mod' pull_request: paths: - 'v3/coraza/**/*.go' - 'v3/coraza/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.26.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/coraza run: go test -v -race ./... ================================================ FILE: .github/workflows/test-fgprof.yml ================================================ name: "Test Fgprof" on: push: branches: - master - main paths: - 'v3/fgprof/**/*.go' - 'v3/fgprof/go.mod' pull_request: paths: - 'v3/fgprof/**/*.go' - 'v3/fgprof/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/fgprof run: go test -v -race ./... ================================================ FILE: .github/workflows/test-hcaptcha.yml ================================================ name: "Test hcaptcha" on: push: branches: - master - main paths: - 'v3/hcaptcha/**/*.go' - 'v3/hcaptcha/go.mod' pull_request: paths: - 'v3/hcaptcha/**/*.go' - 'v3/hcaptcha/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/hcaptcha run: go test -v -race ./... ================================================ FILE: .github/workflows/test-i18n.yml ================================================ name: "Test i18n" on: push: branches: - master - main paths: - 'v3/i18n/**/*.go' - 'v3/i18n/go.mod' pull_request: paths: - 'v3/i18n/**/*.go' - 'v3/i18n/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x - 1.26.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/i18n run: go test -v -race ./... ================================================ FILE: .github/workflows/test-jwt.yml ================================================ name: "Test jwt" on: push: branches: - master - main paths: - 'v3/jwt/**/*.go' - 'v3/jwt/go.mod' pull_request: paths: - 'v3/jwt/**/*.go' - 'v3/jwt/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/jwt run: go test -v -race ./... ================================================ FILE: .github/workflows/test-loadshed.yml ================================================ name: "Test Loadshed" on: push: branches: - master - main paths: - 'v3/loadshed/**/*.go' - 'v3/loadshed/go.mod' pull_request: paths: - 'v3/loadshed/**/*.go' - 'v3/loadshed/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x - 1.26.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: "${{ matrix.go-version }}" - name: Run Test working-directory: ./v3/loadshed run: go test -v -race ./... ================================================ FILE: .github/workflows/test-monitor.yml ================================================ name: "Test Monitor" on: push: branches: - master - main paths: - 'v3/monitor/**/*.go' - 'v3/monitor/go.mod' pull_request: paths: - 'v3/monitor/**/*.go' - 'v3/monitor/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/monitor run: go test -v -race ./... ================================================ FILE: .github/workflows/test-newrelic.yml ================================================ name: "Test newrelic" on: push: branches: - master - main paths: - 'v3/newrelic/**/*.go' - 'v3/newrelic/go.mod' pull_request: paths: - 'v3/newrelic/**/*.go' - 'v3/newrelic/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/newrelic run: go test -v -race ./... ================================================ FILE: .github/workflows/test-opa.yml ================================================ name: "Test opa" on: push: branches: - master - main paths: - 'v3/opa/**/*.go' - 'v3/opa/go.mod' pull_request: paths: - 'v3/opa/**/*.go' - 'v3/opa/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/opa run: go test -v -race ./... ================================================ FILE: .github/workflows/test-otel.yml ================================================ name: "Test otel" on: push: branches: - master - main paths: - 'v3/otel/**/*.go' - 'v3/otel/go.mod' pull_request: paths: - 'v3/otel/**/*.go' - 'v3/otel/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x - 1.26.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/otel run: go test -v -race ./... ================================================ FILE: .github/workflows/test-paseto.yml ================================================ name: "Test paseto" on: push: branches: - master - main paths: - 'v3/paseto/**/*.go' - 'v3/paseto/go.mod' pull_request: paths: - 'v3/paseto/**/*.go' - 'v3/paseto/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/paseto run: go test -v -race ./... ================================================ FILE: .github/workflows/test-sentry.yml ================================================ name: "Test sentry" on: push: branches: - master - main paths: - 'v3/sentry/**/*.go' - 'v3/sentry/go.mod' pull_request: paths: - 'v3/sentry/**/*.go' - 'v3/sentry/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/sentry run: go test -v -race ./... ================================================ FILE: .github/workflows/test-socketio.yml ================================================ name: "Test Socket.io" on: push: branches: - master - main paths: - 'v3/socketio/**/*.go' - 'v3/socketio/go.mod' pull_request: paths: - 'v3/socketio/**/*.go' - 'v3/socketio/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: "${{ matrix.go-version }}" - name: Run Test working-directory: ./v3/socketio run: go test -v -race ./... ================================================ FILE: .github/workflows/test-swaggerui.yml ================================================ name: "Test swaggerui" on: push: branches: - master - main paths: - 'v3/swaggerui/**/*.go' - 'v3/swaggerui/go.mod' - 'v3/swaggerui/go.sum' pull_request: paths: - 'v3/swaggerui/**/*.go' - 'v3/swaggerui/go.mod' - 'v3/swaggerui/go.sum' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/swaggerui run: go test -v -race ./... ================================================ FILE: .github/workflows/test-swaggo.yml ================================================ name: "Test swaggo" on: push: branches: - master - main paths: - 'v3/swaggo/**/*.go' - 'v3/swaggo/go.mod' - 'v3/swaggo/go.sum' pull_request: paths: - 'v3/swaggo/**/*.go' - 'v3/swaggo/go.mod' - 'v3/swaggo/go.sum' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/swaggo run: go test -v -race ./... ================================================ FILE: .github/workflows/test-testcontainers.yml ================================================ name: "Test Testcontainers Services" on: push: branches: - master - main paths: - 'v3/testcontainers/**/*.go' - 'v3/testcontainers/go.mod' pull_request: paths: - 'v3/testcontainers/**/*.go' - 'v3/testcontainers/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/testcontainers run: go test -v -race ./... ================================================ FILE: .github/workflows/test-websocket.yml ================================================ name: "Test websocket" on: push: branches: - master - main paths: - 'v3/websocket/**/*.go' - 'v3/websocket/go.mod' pull_request: paths: - 'v3/websocket/**/*.go' - 'v3/websocket/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x - 1.26.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/websocket run: go test -v -race ./... ================================================ FILE: .github/workflows/test-zap.yml ================================================ name: "Test zap" on: push: branches: - master - main paths: - 'v3/zap/**/*.go' - 'v3/zap/go.mod' pull_request: paths: - 'v3/zap/**/*.go' - 'v3/zap/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/zap run: go test -v -race ./... ================================================ FILE: .github/workflows/test-zerolog.yml ================================================ name: "Test zerolog" on: push: branches: - master - main paths: - 'v3/zerolog/**/*.go' - 'v3/zerolog/go.mod' pull_request: paths: - 'v3/zerolog/**/*.go' - 'v3/zerolog/go.mod' workflow_dispatch: jobs: Tests: runs-on: ubuntu-latest env: GOWORK: off strategy: matrix: go-version: - 1.25.x steps: - name: Fetch Repository uses: actions/checkout@v6 - name: Install Go uses: actions/setup-go@v6 with: go-version: '${{ matrix.go-version }}' - name: Run Test working-directory: ./v3/zerolog run: go test -v -race ./... ================================================ FILE: .github/workflows/weekly-release.yml ================================================ name: Weekly release on: schedule: - cron: '0 8 * * 4' workflow_dispatch: inputs: draft-tags: description: > Tags or package names to publish, comma-separated (e.g. "websocket,jwt" or "v3/websocket/v1.2.0"). Matched as substrings. Empty = all. type: string default: '' dry-run: description: Show diff but do not publish. type: boolean default: false delay: description: Minutes between module publishes (default 2). type: string default: '2' concurrency: group: weekly-release-${{ github.repository }} cancel-in-progress: false permissions: contents: write pull-requests: read issues: write jobs: release: uses: gofiber/.github/.github/workflows/weekly-release.yml@main with: repo-type: multi dry-run: ${{ inputs.dry-run || false }} draft-tags: ${{ inputs.draft-tags || '' }} publish-delay-minutes: ${{ inputs.delay || '2' }} secrets: dispatch-token: ${{ secrets.DISPATCH_TOKEN }} ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test *.tmp # Output of the go coverage tool, specifically when used with LiteIDE *.out # IDE files .vscode .DS_Store .idea # Misc *.fiber.gz *.fasthttp.gz *.pprof *.workspace # Dependencies vendor /Godeps/ # Go workspace file go.work.sum ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Fiber Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ --- title: 👋 Welcome sidebar_position: 1 --- > 📦 The canonical copy of this README lives in [v3/README.md](./v3/README.md).
Fiber Fiber
[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Linter](https://github.com/gofiber/contrib/actions/workflows/lint.yml/badge.svg) Repository for third party middlewares and service implementations, with dependencies. > **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.
## 📑 Middleware Implementations * [casbin](./v3/casbin/README.md) casbin workflow status * [circuitbreaker](./v3/circuitbreaker/README.md) circuitbreaker workflow status * [fgprof](./v3/fgprof/README.md) fgprof workflow status * [i18n](./v3/i18n/README.md) i18n workflow status * [sentry](./v3/sentry/README.md) sentry workflow status * [zap](./v3/zap/README.md) zap workflow status * [zerolog](./v3/zerolog/README.md) zerolog workflow status * [hcaptcha](./v3/hcaptcha/README.md) hcaptcha workflow status * [jwt](./v3/jwt/README.md) jwt workflow status * [loadshed](./v3/loadshed/README.md) loadshed workflow status * [new relic](./v3/newrelic/README.md) new relic workflow status * [monitor](./v3/monitor/README.md) monitor workflow status * [open policy agent](./v3/opa/README.md) OPA workflow status * [otel (opentelemetry)](./v3/otel/README.md) otel workflow status * [paseto](./v3/paseto/README.md) paseto workflow status * [socket.io](./v3/socketio/README.md) socket.io workflow status * [swaggo](./v3/swaggo/README.md) swaggo workflow status _(formerly `gofiber/swagger`)_ * [swaggerui](./v3/swaggerui/README.md) swaggerui workflow status _(formerly `gofiber/contrib/swagger`)_ * [websocket](./v3/websocket/README.md) websocket workflow status ## 🥡 Service Implementations * [testcontainers](./v3/testcontainers/README.md) testcontainers workflow status ================================================ FILE: go.work ================================================ go 1.26.1 use ( ./v3/casbin ./v3/circuitbreaker ./v3/coraza ./v3/fgprof ./v3/hcaptcha ./v3/i18n ./v3/jwt ./v3/loadshed ./v3/monitor ./v3/newrelic ./v3/opa ./v3/otel ./v3/paseto ./v3/sentry ./v3/socketio ./v3/swaggerui ./v3/swaggo ./v3/testcontainers ./v3/websocket ./v3/zap ./v3/zerolog ) ================================================ FILE: v3/.golangci.yml ================================================ version: "2" run: go: "1.25" timeout: 5m tests: false ================================================ FILE: v3/README.md ================================================ --- title: 👋 Welcome sidebar_position: 1 ---
Fiber Fiber
[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Linter](https://github.com/gofiber/contrib/actions/workflows/lint.yml/badge.svg) Repository for third party middlewares and service implementations, with dependencies. > **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.
## 📑 Middleware Implementations * [casbin](./casbin/README.md) casbin workflow status * [circuitbreaker](./circuitbreaker/README.md) circuitbreaker workflow status * [coraza](./coraza/README.md) coraza workflow status * [fgprof](./fgprof/README.md) fgprof workflow status * [i18n](./i18n/README.md) i18n workflow status * [sentry](./sentry/README.md) sentry workflow status * [zap](./zap/README.md) zap workflow status * [zerolog](./zerolog/README.md) zerolog workflow status * [hcaptcha](./hcaptcha/README.md) hcaptcha workflow status * [jwt](./jwt/README.md) jwt workflow status * [loadshed](./loadshed/README.md) loadshed workflow status * [new relic](./newrelic/README.md) new relic workflow status * [monitor](./monitor/README.md) monitor workflow status * [open policy agent](./opa/README.md) OPA workflow status * [otel (opentelemetry)](./otel/README.md) otel workflow status * [paseto](./paseto/README.md) paseto workflow status * [socket.io](./socketio/README.md) socket.io workflow status * [swaggo](./swaggo/README.md) swaggo workflow status _(formerly `gofiber/swagger`)_ * [swaggerui](./swaggerui/README.md) swaggerui workflow status _(formerly `gofiber/contrib/swagger`)_ * [websocket](./websocket/README.md) websocket workflow status ## 🥡 Service Implementations * [testcontainers](./testcontainers/README.md) testcontainers workflow status ================================================ FILE: v3/casbin/README.md ================================================ --- id: casbin --- # Casbin ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*casbin*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20casbin/badge.svg) Casbin middleware for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/casbin ``` choose an adapter from [here](https://casbin.org/docs/adapters) ```sh go get -u github.com/casbin/xorm-adapter ``` ## Signature ```go casbin.New(config ...casbin.Config) *casbin.Middleware ``` ## Config | Property | Type | Description | Default | |:--------------|:--------------------------|:-----------------------------------------|:--------------------------------------------------------------| | ModelFilePath | `string` | Model file path | `"./model.conf"` | | PolicyAdapter | `persist.Adapter` | Database adapter for policies | `./policy.csv` | | Enforcer | `*casbin.Enforcer` | Custom casbin enforcer | `Middleware generated enforcer using ModelFilePath & PolicyAdapter` | | Lookup | `func(fiber.Ctx) string` | Look up for current subject | `""` | | Unauthorized | `func(fiber.Ctx) error` | Response body for unauthorized responses | `Unauthorized` | | Forbidden | `func(fiber.Ctx) error` | Response body for forbidden responses | `Forbidden` | ### Examples - [Gorm Adapter](https://github.com/svcg/-fiber_casbin_demo) - [File Adapter](https://github.com/gofiber/contrib/tree/master/v3/casbin/example) ## CustomPermission ```go package main import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/casbin" _ "github.com/go-sql-driver/mysql" "github.com/casbin/xorm-adapter/v2" ) func main() { app := fiber.New() authz := casbin.New(casbin.Config{ ModelFilePath: "path/to/rbac_model.conf", PolicyAdapter: xormadapter.NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/"), Lookup: func(c fiber.Ctx) string { // fetch authenticated user subject }, }) app.Post("/blog", authz.RequiresPermissions([]string{"blog:create"}, casbin.WithValidationRule(casbin.MatchAllRule)), func(c fiber.Ctx) error { // your handler }, ) app.Delete("/blog/:id", authz.RequiresPermissions([]string{"blog:create", "blog:delete"}, casbin.WithValidationRule(casbin.AtLeastOneRule)), func(c fiber.Ctx) error { // your handler }, ) app.Listen(":8080") } ``` ## RoutePermission ```go package main import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/casbin" _ "github.com/go-sql-driver/mysql" "github.com/casbin/xorm-adapter/v2" ) func main() { app := fiber.New() authz := casbin.New(casbin.Config{ ModelFilePath: "path/to/rbac_model.conf", PolicyAdapter: xormadapter.NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/"), Lookup: func(c fiber.Ctx) string { // fetch authenticated user subject }, }) // check permission with Method and Path app.Post("/blog", authz.RoutePermission(), func(c fiber.Ctx) error { // your handler }, ) app.Listen(":8080") } ``` ## RoleAuthorization ```go package main import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/casbin" _ "github.com/go-sql-driver/mysql" "github.com/casbin/xorm-adapter/v2" ) func main() { app := fiber.New() authz := casbin.New(casbin.Config{ ModelFilePath: "path/to/rbac_model.conf", PolicyAdapter: xormadapter.NewAdapter("mysql", "root:@tcp(127.0.0.1:3306)/"), Lookup: func(c fiber.Ctx) string { // fetch authenticated user subject }, }) app.Put("/blog/:id", authz.RequiresRoles([]string{"admin"}), func(c fiber.Ctx) error { // your handler }, ) app.Listen(":8080") } ``` ================================================ FILE: v3/casbin/casbin.go ================================================ package casbin import ( "fmt" "github.com/gofiber/fiber/v3" ) // Middleware ... type Middleware struct { config Config } // New creates an authorization middleware for use in Fiber func New(config ...Config) *Middleware { cfg, err := configDefault(config...) if err != nil { panic(fmt.Errorf("fiber: casbin middleware error -> %w", err)) } return &Middleware{ config: cfg, } } // RequiresPermissions tries to find the current subject and determine if the // subject has the required permissions according to predefined Casbin policies. func (m *Middleware) RequiresPermissions(permissions []string, opts ...Option) fiber.Handler { options := optionsDefault(opts...) return func(c fiber.Ctx) error { if len(permissions) == 0 { return c.Next() } sub := m.config.Lookup(c) if len(sub) == 0 { return m.config.Unauthorized(c) } switch options.ValidationRule { case MatchAllRule: for _, permission := range permissions { vals := append([]string{sub}, options.PermissionParser(permission)...) if ok, err := m.config.Enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil { return c.SendStatus(fiber.StatusInternalServerError) } else if !ok { return m.config.Forbidden(c) } } return c.Next() case AtLeastOneRule: for _, permission := range permissions { vals := append([]string{sub}, options.PermissionParser(permission)...) if ok, err := m.config.Enforcer.Enforce(stringSliceToInterfaceSlice(vals)...); err != nil { return c.SendStatus(fiber.StatusInternalServerError) } else if ok { return c.Next() } } return m.config.Forbidden(c) } return c.Next() } } // RoutePermission tries to find the current subject and determine if the // subject has the required permissions according to predefined Casbin policies. // This method uses http Path and Method as object and action. func (m *Middleware) RoutePermission() fiber.Handler { return func(c fiber.Ctx) error { sub := m.config.Lookup(c) if len(sub) == 0 { return m.config.Unauthorized(c) } if ok, err := m.config.Enforcer.Enforce(sub, c.Path(), c.Method()); err != nil { return c.SendStatus(fiber.StatusInternalServerError) } else if !ok { return m.config.Forbidden(c) } return c.Next() } } // RequiresRoles tries to find the current subject and determine if the // subject has the required roles according to predefined Casbin policies. func (m *Middleware) RequiresRoles(roles []string, opts ...Option) fiber.Handler { options := optionsDefault(opts...) return func(c fiber.Ctx) error { if len(roles) == 0 { return c.Next() } sub := m.config.Lookup(c) if len(sub) == 0 { return m.config.Unauthorized(c) } userRoles, err := m.config.Enforcer.GetRolesForUser(sub) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) } switch options.ValidationRule { case MatchAllRule: for _, role := range roles { if !containsString(userRoles, role) { return m.config.Forbidden(c) } } return c.Next() case AtLeastOneRule: for _, role := range roles { if containsString(userRoles, role) { return c.Next() } } return m.config.Forbidden(c) } return c.Next() } } ================================================ FILE: v3/casbin/casbin_test.go ================================================ package casbin import ( "errors" "net/http" "strings" "testing" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/persist" "github.com/gofiber/fiber/v3" ) var ( subjectAlice = func(c fiber.Ctx) string { return "alice" } subjectBob = func(c fiber.Ctx) string { return "bob" } subjectEmpty = func(c fiber.Ctx) string { return "" } ) const ( modelConf = ` [request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act` policyList = ` p,admin,blog,create p,admin,blog,update p,admin,blog,delete p,user,comment,create p,user,comment,delete p,admin,/blog,POST p,admin,/blog/1,PUT p,admin,/blog/1,DELETE p,user,/comment,POST g,alice,admin g,alice,user g,bob,user` ) // mockAdapter type mockAdapter struct { text string } func newMockAdapter(text string) *mockAdapter { return &mockAdapter{ text: text, } } func (ma *mockAdapter) LoadPolicy(model model.Model) error { if ma.text == "" { return errors.New("text is required") } strs := strings.Split(ma.text, "\n") for _, str := range strs { if str == "" { continue } persist.LoadPolicyLine(str, model) } return nil } func (ma *mockAdapter) SavePolicy(model model.Model) error { return errors.New("not implemented") } func (ma *mockAdapter) AddPolicy(sec string, ptype string, rule []string) error { return errors.New("not implemented") } func (ma *mockAdapter) RemovePolicy(sec string, ptype string, rule []string) error { return errors.New("not implemented") } func (ma *mockAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { return errors.New("not implemented") } func setup() (*casbin.Enforcer, error) { m, err := model.NewModelFromString(modelConf) if err != nil { return nil, err } enf, err := casbin.NewEnforcer(m, newMockAdapter(policyList)) if err != nil { return nil, err } return enf, nil } func Test_RequiresPermission(t *testing.T) { enf, err := setup() if err != nil { t.Fatal(err) } testCases := []struct { desc string lookup func(fiber.Ctx) string permissions []string opts []Option statusCode int }{ { desc: "alice has permission to create blog", lookup: subjectAlice, permissions: []string{"blog:create"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 200, }, { desc: "alice has permission to create blog", lookup: subjectAlice, permissions: []string{"blog:create"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "alice has permission to create and update blog", lookup: subjectAlice, permissions: []string{"blog:create", "blog:update"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 200, }, { desc: "alice has permission to create comment or blog", lookup: subjectAlice, permissions: []string{"comment:create", "blog:create"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "bob has only permission to create comment", lookup: subjectBob, permissions: []string{"comment:create", "blog:create"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "unauthenticated user has no permissions", lookup: subjectEmpty, permissions: []string{"comment:create"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 401, }, { desc: "bob has not permission to create blog", lookup: subjectBob, permissions: []string{"blog:create"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 403, }, { desc: "bob has not permission to delete blog", lookup: subjectBob, permissions: []string{"blog:delete"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 403, }, { desc: "invalid permission", lookup: subjectBob, permissions: []string{"unknown"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 500, }, } for _, tC := range testCases { app := fiber.New() authz := New(Config{ Enforcer: enf, Lookup: tC.lookup, }) app.Post("/blog", authz.RequiresPermissions(tC.permissions, tC.opts...), func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }, ) t.Run(tC.desc, func(t *testing.T) { req, err := http.NewRequest("POST", "/blog", nil) if err != nil { t.Fatal(err) } req.Host = "localhost" resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != tC.statusCode { t.Errorf(`StatusCode: got %v - expected %v`, resp.StatusCode, tC.statusCode) } }) } } func Test_RequiresRoles(t *testing.T) { enf, err := setup() if err != nil { t.Fatal(err) } testCases := []struct { desc string lookup func(fiber.Ctx) string roles []string opts []Option statusCode int }{ { desc: "alice has user role", lookup: subjectAlice, roles: []string{"user"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 200, }, { desc: "alice has admin role", lookup: subjectAlice, roles: []string{"admin"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "alice has both user and admin roles", lookup: subjectAlice, roles: []string{"user", "admin"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 200, }, { desc: "alice has both user and admin roles", lookup: subjectAlice, roles: []string{"user", "admin"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "bob has only user role", lookup: subjectBob, roles: []string{"user"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "unauthenticated user has no permissions", lookup: subjectEmpty, roles: []string{"user"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 401, }, { desc: "bob has not admin role", lookup: subjectBob, roles: []string{"admin"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 403, }, { desc: "bob has only user role", lookup: subjectBob, roles: []string{"admin", "user"}, opts: []Option{WithValidationRule(AtLeastOneRule)}, statusCode: 200, }, { desc: "invalid role", lookup: subjectBob, roles: []string{"unknown"}, opts: []Option{WithValidationRule(MatchAllRule)}, statusCode: 403, }, } for _, tC := range testCases { app := fiber.New() authz := New(Config{ Enforcer: enf, Lookup: tC.lookup, }) app.Post("/blog", authz.RequiresRoles(tC.roles, tC.opts...), func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }, ) t.Run(tC.desc, func(t *testing.T) { req, err := http.NewRequest("POST", "/blog", nil) if err != nil { t.Fatal(err) } req.Host = "localhost" resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != tC.statusCode { t.Errorf(`StatusCode: got %v - expected %v`, resp.StatusCode, tC.statusCode) } }) } } func Test_RoutePermission(t *testing.T) { enf, err := setup() if err != nil { t.Fatal(err) } testCases := []struct { desc string url string method string subject string statusCode int }{ { desc: "alice has permission to create blog", url: "/blog", method: "POST", subject: "alice", statusCode: 200, }, { desc: "alice has permission to update blog", url: "/blog/1", method: "PUT", subject: "alice", statusCode: 200, }, { desc: "bob has only permission to create comment", url: "/comment", method: "POST", subject: "bob", statusCode: 200, }, { desc: "unauthenticated user has no permissions", url: "/", method: "POST", subject: "", statusCode: 401, }, { desc: "bob has not permission to create blog", url: "/blog", method: "POST", subject: "bob", statusCode: 403, }, { desc: "bob has not permission to delete blog", url: "/blog/1", method: "DELETE", subject: "bob", statusCode: 403, }, } app := fiber.New() authz := New(Config{ Enforcer: enf, Lookup: func(c fiber.Ctx) string { return c.Get("x-subject") }, }) app.Use(authz.RoutePermission()) app.Post("/blog", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }, ) app.Put("/blog/:id", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }, ) app.Delete("/blog/:id", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }, ) app.Post("/comment", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }, ) for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { req, err := http.NewRequest(tC.method, tC.url, nil) if err != nil { t.Fatal(err) } req.Host = "localhost" req.Header.Set("x-subject", tC.subject) resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != tC.statusCode { t.Errorf(`StatusCode: got %v - expected %v`, resp.StatusCode, tC.statusCode) } }) } } ================================================ FILE: v3/casbin/config.go ================================================ package casbin import ( "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/persist" fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" "github.com/gofiber/fiber/v3" ) // Config holds the configuration for the middleware type Config struct { // ModelFilePath is path to model file for Casbin. // Optional. Default: "./model.conf". ModelFilePath string // PolicyAdapter is an interface for different persistent providers. // Optional. Default: fileadapter.NewAdapter("./policy.csv"). PolicyAdapter persist.Adapter // Enforcer is an enforcer. If you want to use your own enforcer. // Optional. Default: nil Enforcer *casbin.Enforcer // Lookup is a function that is used to look up current subject. // An empty string is considered as unauthenticated user. // Optional. Default: func(c fiber.Ctx) string { return "" } Lookup func(fiber.Ctx) string // Unauthorized defines the response body for unauthorized responses. // Optional. Default: func(c fiber.Ctx) error { return c.SendStatus(401) } Unauthorized fiber.Handler // Forbidden defines the response body for forbidden responses. // Optional. Default: func(c fiber.Ctx) error { return c.SendStatus(403) } Forbidden fiber.Handler } var ConfigDefault = Config{ ModelFilePath: "./model.conf", PolicyAdapter: fileadapter.NewAdapter("./policy.csv"), Lookup: func(c fiber.Ctx) string { return "" }, Unauthorized: func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusUnauthorized) }, Forbidden: func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusForbidden) }, } // Helper function to set default values func configDefault(config ...Config) (Config, error) { // Return default config if nothing provided if len(config) < 1 { return ConfigDefault, nil } // Override default config cfg := config[0] if cfg.Enforcer == nil { if cfg.ModelFilePath == "" { cfg.ModelFilePath = ConfigDefault.ModelFilePath } if cfg.PolicyAdapter == nil { cfg.PolicyAdapter = ConfigDefault.PolicyAdapter } enforcer, err := casbin.NewEnforcer(cfg.ModelFilePath, cfg.PolicyAdapter) if err != nil { return cfg, err } cfg.Enforcer = enforcer } if cfg.Lookup == nil { cfg.Lookup = ConfigDefault.Lookup } if cfg.Unauthorized == nil { cfg.Unauthorized = ConfigDefault.Unauthorized } if cfg.Forbidden == nil { cfg.Forbidden = ConfigDefault.Forbidden } return cfg, nil } ================================================ FILE: v3/casbin/go.mod ================================================ module github.com/gofiber/contrib/v3/casbin go 1.25.0 require ( github.com/casbin/casbin/v2 v2.135.0 github.com/gofiber/fiber/v3 v3.1.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect ) ================================================ FILE: v3/casbin/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/casbin/options.go ================================================ package casbin import "strings" const ( MatchAllRule ValidationRule = iota AtLeastOneRule ) var OptionsDefault = Options{ ValidationRule: MatchAllRule, PermissionParser: PermissionParserWithSeperator(":"), } type ( ValidationRule int // PermissionParserFunc is used for parsing the permission // to extract object and action usually PermissionParserFunc func(str string) []string OptionFunc func(*Options) // Option specifies casbin configuration options. Option interface { apply(*Options) } // Options holds Options of middleware Options struct { ValidationRule ValidationRule PermissionParser PermissionParserFunc } ) func (of OptionFunc) apply(o *Options) { of(o) } func WithValidationRule(vr ValidationRule) Option { return OptionFunc(func(o *Options) { o.ValidationRule = vr }) } func WithPermissionParser(pp PermissionParserFunc) Option { return OptionFunc(func(o *Options) { o.PermissionParser = pp }) } func PermissionParserWithSeperator(sep string) PermissionParserFunc { return func(str string) []string { return strings.Split(str, sep) } } // Helper function to set default values func optionsDefault(opts ...Option) Options { cfg := OptionsDefault for _, opt := range opts { opt.apply(&cfg) } return cfg } ================================================ FILE: v3/casbin/utils.go ================================================ package casbin func containsString(s []string, v string) bool { for _, vv := range s { if vv == v { return true } } return false } func stringSliceToInterfaceSlice(s []string) []interface{} { res := make([]interface{}, len(s)) for i, v := range s { res[i] = v } return res } ================================================ FILE: v3/circuitbreaker/README.md ================================================ --- id: circuitbreaker --- # Circuit Breaker ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*circuitbreaker*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20CircuitBreaker/badge.svg) A **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. **Compatible with Fiber v3.** ## 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. ## How It Works 1. **Closed State:** - Requests are allowed to pass normally. - Failures are counted. - If failures exceed a defined **threshold**, the circuit switches to **Open** state. 2. **Open State:** - Requests are **blocked immediately** to prevent overload. - The circuit stays open for a **timeout period** before moving to **Half-Open**. 3. **Half-Open State:** - Allows a limited number of requests to test service recovery. - If requests **succeed**, the circuit resets to **Closed**. - If requests **fail**, the circuit returns to **Open**. ## Benefits of Using a Circuit Breaker ✅ **Prevents cascading failures** in microservices. ✅ **Improves system reliability** by avoiding repeated failed requests. ✅ **Reduces load on struggling services** and allows recovery. ## Install ```bash go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/circuitbreaker ``` ## Signature ```go circuitbreaker.New(config ...circuitbreaker.Config) *circuitbreaker.Middleware ``` ## Config | Property | Type | Description | Default | |:---------|:-----|:------------|:--------| | FailureThreshold | `int` | Number of consecutive errors required to open the circuit | `5` | | Timeout | `time.Duration` | Timeout for the circuit breaker | `10 * time.Second` | | SuccessThreshold | `int` | Number of successful requests required to close the circuit | `5` | | HalfOpenMaxConcurrent | `int` | Max concurrent requests in half-open state | `1` | | IsFailure | `func(error) bool` | Custom function to determine if an error is a failure | `Status >= 500` | | OnOpen | `func(fiber.Ctx) error` | Callback function when the circuit is opened | `503 response` | | OnClose | `func(fiber.Ctx) error` | Callback function when the circuit is closed | `Continue request` | | OnHalfOpen | `func(fiber.Ctx) error` | Callback function when the circuit is half-open | `429 response` | ## Circuit Breaker Usage in Fiber (Example) This guide explains how to use a Circuit Breaker in a Fiber application at different levels, from basic setup to advanced customization. ### 1. Basic Setup A **global** Circuit Breaker protects all routes. **Example: Applying Circuit Breaker to All Routes** ```go package main import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/circuitbreaker" ) func main() { app := fiber.New() // Create a new Circuit Breaker with custom configuration cb := circuitbreaker.New(circuitbreaker.Config{ FailureThreshold: 3, // Max failures before opening the circuit Timeout: 5 * time.Second, // Wait time before retrying SuccessThreshold: 2, // Required successes to move back to closed state }) // Apply Circuit Breaker to ALL routes app.Use(circuitbreaker.Middleware(cb)) // Sample Route app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, world!") }) // Optional: Expose health check endpoint app.Get("/health/circuit", cb.HealthHandler()) // Optional: Expose metrics about the circuit breaker: app.Get("/metrics/circuit", func(c fiber.Ctx) error { return c.JSON(cb.GetStateStats()) }) app.Listen(":3000") // In your application shutdown logic app.Shutdown(func() { // Make sure to stop the circuit breaker when your application shuts down: cb.Stop() }) } ``` ### 2. Route & Route-Group Specific Circuit Breaker Apply the Circuit Breaker **only to specific routes**. ```go app.Get("/protected", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error { return c.SendString("Protected service running") }) ``` Apply the Circuit Breaker **only to specific routes groups**. ```go app := route.Group("/api") app.Use(circuitbreaker.Middleware(cb)) // All routes in this group will be protected app.Get("/users", getUsersHandler) app.Post("/users", createUserHandler) ``` ### 3. Circuit Breaker with Custom Failure Handling Customize the response when the circuit **opens**. ```go cb := circuitbreaker.New(circuitbreaker.Config{ FailureThreshold: 3, Timeout: 10 * time.Second, OnOpen: func(c fiber.Ctx) error { return c.Status(fiber.StatusServiceUnavailable). JSON(fiber.Map{"error": "Circuit Open: Service unavailable"}) }, OnHalfOpen: func(c fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests). JSON(fiber.Map{"error": "Circuit Half-Open: Retrying service"}) }, OnClose: func(c fiber.Ctx) error { return c.Status(fiber.StatusOK). JSON(fiber.Map{"message": "Circuit Closed: Service recovered"}) }, }) // Apply to a specific route app.Get("/custom", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error { return c.SendString("This service is protected by a Circuit Breaker") }) ``` ✅ Now, when failures exceed the threshold, ***custom error responses** will be sent. ### 4. Circuit Breaker for External API Calls Use a Circuit Breaker **when calling an external API.** ```go app.Get("/external-api", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error { // Simulating an external API call resp, err := fiber.Get("https://example.com/api") if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "External API failed") } return c.SendString(resp.Body()) }) ``` ✅ If the external API fails repeatedly, **the circuit breaker prevents further calls.** ### 5. Circuit Breaker with Concurrent Requests Handling Use a **semaphore-based** approach to **limit concurrent requests.** ```go cb := circuitbreaker.New(circuitbreaker.Config{ FailureThreshold: 3, Timeout: 5 * time.Second, SuccessThreshold: 2, HalfOpenSemaphore: make(chan struct{}, 2), // Allow only 2 concurrent requests }) app.Get("/half-open-limit", circuitbreaker.Middleware(cb), func(c fiber.Ctx) error { time.Sleep(2 * time.Second) // Simulating slow response return c.SendString("Half-Open: Limited concurrent requests") }) ``` ✅ When in **half-open** state, only **2 concurrent requests are allowed**. ### 6. Circuit Breaker with Custom Metrics Integrate **Prometheus metrics** and **structured logging**. ```go cb := circuitbreaker.New(circuitbreaker.Config{ FailureThreshold: 5, Timeout: 10 * time.Second, OnOpen: func(c fiber.Ctx) error { log.Println("Circuit Breaker Opened!") prometheus.Inc("circuit_breaker_open_count") return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Service Down"}) }, }) ``` ✅ Logs when the circuit opens & increments Prometheus metrics. ### 7. Advanced: Multiple Circuit Breakers for Different Services Use different Circuit Breakers for different services. ```go dbCB := circuitbreaker.New(circuitbreaker.Config{FailureThreshold: 5, Timeout: 10 * time.Second}) apiCB := circuitbreaker.New(circuitbreaker.Config{FailureThreshold: 3, Timeout: 5 * time.Second}) app.Get("/db-service", circuitbreaker.Middleware(dbCB), func(c fiber.Ctx) error { return c.SendString("DB service request") }) app.Get("/api-service", circuitbreaker.Middleware(apiCB), func(c fiber.Ctx) error { return c.SendString("External API service request") }) ``` ✅ Each service has its own failure threshold & timeout. ================================================ FILE: v3/circuitbreaker/circuitbreaker.go ================================================ package circuitbreaker import ( "context" "net/http" "sync" "sync/atomic" "time" "github.com/gofiber/fiber/v3" ) // State represents the state of the circuit breaker type State string const ( StateClosed State = "closed" // Normal operation StateOpen State = "open" // Requests are blocked StateHalfOpen State = "half-open" // Limited requests allowed to check recovery ) // Config holds the configurable parameters type Config struct { // Failure threshold to trip the circuit FailureThreshold int // Duration circuit stays open before allowing test requests Timeout time.Duration // Success threshold to close the circuit from half-open SuccessThreshold int // Maximum concurrent requests allowed in half-open state HalfOpenMaxConcurrent int // Custom failure detector function (return true if response should count as failure) IsFailure func(c fiber.Ctx, err error) bool // Callbacks for state transitions OnOpen func(fiber.Ctx) error // Called when circuit opens OnHalfOpen func(fiber.Ctx) error // Called when circuit transitions to half-open OnClose func(fiber.Ctx) error // Called when circuit closes } // DefaultConfig provides sensible defaults for the circuit breaker var DefaultConfig = Config{ FailureThreshold: 5, Timeout: 5 * time.Second, SuccessThreshold: 1, HalfOpenMaxConcurrent: 1, IsFailure: func(c fiber.Ctx, err error) bool { return err != nil || c.Response().StatusCode() >= http.StatusInternalServerError }, OnOpen: func(c fiber.Ctx) error { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "service unavailable", }) }, OnHalfOpen: func(c fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "error": "service under recovery", }) }, OnClose: func(c fiber.Ctx) error { return c.Next() }, } // CircuitBreaker implements the circuit breaker pattern type CircuitBreaker struct { failureCount int64 // Count of failures (atomic) successCount int64 // Count of successes in half-open state (atomic) totalRequests int64 // Count of total requests (atomic) rejectedRequests int64 // Count of rejected requests (atomic) state State // Current state of circuit breaker mutex sync.RWMutex // Protects state transitions failureThreshold int // Max failures before opening circuit timeout time.Duration // Duration to stay open before transitioning to half-open successThreshold int // Successes required to close circuit openTimer *time.Timer // Timer for state transition from open to half-open ctx context.Context // Context for cancellation cancel context.CancelFunc // Cancel function for cleanup config Config // Configuration settings now func() time.Time // Function for getting current time (useful for testing) halfOpenSemaphore chan struct{} // Controls limited requests in half-open state lastStateChange time.Time // Time of last state change } // New initializes a circuit breaker with the given configuration func New(config Config) *CircuitBreaker { // Apply default values for zero values if config.FailureThreshold <= 0 { config.FailureThreshold = DefaultConfig.FailureThreshold } if config.Timeout <= 0 { config.Timeout = DefaultConfig.Timeout } if config.SuccessThreshold <= 0 { config.SuccessThreshold = DefaultConfig.SuccessThreshold } if config.HalfOpenMaxConcurrent <= 0 { config.HalfOpenMaxConcurrent = DefaultConfig.HalfOpenMaxConcurrent } if config.IsFailure == nil { config.IsFailure = DefaultConfig.IsFailure } if config.OnOpen == nil { config.OnOpen = DefaultConfig.OnOpen } if config.OnHalfOpen == nil { config.OnHalfOpen = DefaultConfig.OnHalfOpen } if config.OnClose == nil { config.OnClose = DefaultConfig.OnClose } ctx, cancel := context.WithCancel(context.Background()) now := time.Now() return &CircuitBreaker{ failureThreshold: config.FailureThreshold, timeout: config.Timeout, successThreshold: config.SuccessThreshold, state: StateClosed, ctx: ctx, cancel: cancel, config: config, now: time.Now, halfOpenSemaphore: make(chan struct{}, config.HalfOpenMaxConcurrent), lastStateChange: now, totalRequests: 0, rejectedRequests: 0, } } // Stop cancels the circuit breaker and releases resources func (cb *CircuitBreaker) Stop() { cb.mutex.Lock() defer cb.mutex.Unlock() if cb.openTimer != nil { cb.openTimer.Stop() } cb.cancel() } // GetState returns the current state of the circuit breaker func (cb *CircuitBreaker) GetState() State { cb.mutex.RLock() defer cb.mutex.RUnlock() return cb.state } // IsOpen returns true if the circuit is open func (cb *CircuitBreaker) IsOpen() bool { return cb.GetState() == StateOpen } // Reset resets the circuit breaker to its initial closed state func (cb *CircuitBreaker) Reset() { cb.mutex.Lock() defer cb.mutex.Unlock() // Reset counters atomic.StoreInt64(&cb.failureCount, 0) atomic.StoreInt64(&cb.successCount, 0) // Reset state cb.state = StateClosed cb.lastStateChange = cb.now() // Cancel any pending state transitions if cb.openTimer != nil { cb.openTimer.Stop() } } // ForceOpen forcibly opens the circuit regardless of failure count func (cb *CircuitBreaker) ForceOpen() { cb.transitionToOpen() } // ForceClose forcibly closes the circuit regardless of current state func (cb *CircuitBreaker) ForceClose() { cb.mutex.Lock() defer cb.mutex.Unlock() cb.state = StateClosed cb.lastStateChange = cb.now() atomic.StoreInt64(&cb.failureCount, 0) atomic.StoreInt64(&cb.successCount, 0) if cb.openTimer != nil { cb.openTimer.Stop() } } // SetTimeout updates the timeout duration func (cb *CircuitBreaker) SetTimeout(timeout time.Duration) { cb.mutex.Lock() defer cb.mutex.Unlock() cb.timeout = timeout } // transitionToOpen changes state to open and schedules transition to half-open func (cb *CircuitBreaker) transitionToOpen() { cb.mutex.Lock() defer cb.mutex.Unlock() if cb.state != StateOpen { cb.state = StateOpen cb.lastStateChange = cb.now() // Stop existing timer if any if cb.openTimer != nil { cb.openTimer.Stop() } // Schedule transition to half-open after timeout cb.openTimer = time.AfterFunc(cb.timeout, func() { cb.transitionToHalfOpen() }) // Reset failure counter atomic.StoreInt64(&cb.failureCount, 0) } } // transitionToHalfOpen changes state from open to half-open func (cb *CircuitBreaker) transitionToHalfOpen() { cb.mutex.Lock() defer cb.mutex.Unlock() if cb.state == StateOpen { cb.state = StateHalfOpen cb.lastStateChange = cb.now() // Reset counters atomic.StoreInt64(&cb.failureCount, 0) atomic.StoreInt64(&cb.successCount, 0) // Empty the semaphore channel select { case <-cb.halfOpenSemaphore: default: } } } // transitionToClosed changes state from half-open to closed func (cb *CircuitBreaker) transitionToClosed() { cb.mutex.Lock() defer cb.mutex.Unlock() if cb.state == StateHalfOpen { cb.state = StateClosed cb.lastStateChange = cb.now() // Reset counters atomic.StoreInt64(&cb.failureCount, 0) atomic.StoreInt64(&cb.successCount, 0) } } // AllowRequest determines if a request is allowed based on circuit state func (cb *CircuitBreaker) AllowRequest() (bool, State) { atomic.AddInt64(&cb.totalRequests, 1) cb.mutex.RLock() state := cb.state cb.mutex.RUnlock() switch state { case StateOpen: atomic.AddInt64(&cb.rejectedRequests, 1) return false, state case StateHalfOpen: select { case cb.halfOpenSemaphore <- struct{}{}: return true, state default: atomic.AddInt64(&cb.rejectedRequests, 1) return false, state } default: // StateClosed return true, state } } // ReleaseSemaphore releases a slot in the half-open semaphore func (cb *CircuitBreaker) ReleaseSemaphore() { select { case <-cb.halfOpenSemaphore: default: } } // ReportSuccess increments success count and closes circuit if threshold met func (cb *CircuitBreaker) ReportSuccess() { cb.mutex.RLock() currentState := cb.state cb.mutex.RUnlock() if currentState == StateHalfOpen { newSuccessCount := atomic.AddInt64(&cb.successCount, 1) if int(newSuccessCount) >= cb.successThreshold { cb.transitionToClosed() } } } // ReportFailure increments failure count and opens circuit if threshold met func (cb *CircuitBreaker) ReportFailure() { cb.mutex.RLock() currentState := cb.state cb.mutex.RUnlock() switch currentState { case StateHalfOpen: // In half-open, a single failure trips the circuit cb.transitionToOpen() case StateClosed: newFailureCount := atomic.AddInt64(&cb.failureCount, 1) if int(newFailureCount) >= cb.failureThreshold { cb.transitionToOpen() } } } // Metrics returns basic metrics about the circuit breaker func (cb *CircuitBreaker) Metrics() fiber.Map { return fiber.Map{ "state": cb.GetState(), "failures": atomic.LoadInt64(&cb.failureCount), "successes": atomic.LoadInt64(&cb.successCount), "totalRequests": atomic.LoadInt64(&cb.totalRequests), "rejectedRequests": atomic.LoadInt64(&cb.rejectedRequests), } } // GetStateStats returns detailed statistics about the circuit breaker func (cb *CircuitBreaker) GetStateStats() fiber.Map { state := cb.GetState() return fiber.Map{ "state": state, "failures": atomic.LoadInt64(&cb.failureCount), "successes": atomic.LoadInt64(&cb.successCount), "totalRequests": atomic.LoadInt64(&cb.totalRequests), "rejectedRequests": atomic.LoadInt64(&cb.rejectedRequests), "lastStateChange": cb.lastStateChange, "openDuration": cb.timeout, "failureThreshold": cb.failureThreshold, "successThreshold": cb.successThreshold, } } // HealthHandler returns a Fiber handler for checking circuit breaker status func (cb *CircuitBreaker) HealthHandler() fiber.Handler { return func(c fiber.Ctx) error { state := cb.GetState() data := fiber.Map{ "state": state, "healthy": state == StateClosed, } if state == StateOpen { return c.Status(fiber.StatusServiceUnavailable).JSON(data) } return c.JSON(data) } } // Middleware wraps the fiber handler with circuit breaker logic func Middleware(cb *CircuitBreaker) fiber.Handler { return func(c fiber.Ctx) error { allowed, state := cb.AllowRequest() if !allowed { // Call appropriate callback based on state if state == StateHalfOpen && cb.config.OnHalfOpen != nil { return cb.config.OnHalfOpen(c) } else if state == StateOpen && cb.config.OnOpen != nil { return cb.config.OnOpen(c) } return c.SendStatus(fiber.StatusServiceUnavailable) } // If request allowed in half-open state, ensure semaphore is released halfOpen := state == StateHalfOpen if halfOpen { defer cb.ReleaseSemaphore() } // Execute the request err := c.Next() // Check if the response should be considered a failure if cb.config.IsFailure(c, err) { cb.ReportFailure() } else { cb.ReportSuccess() // If transition to closed state just happened, trigger callback if halfOpen && cb.GetState() == StateClosed && cb.config.OnClose != nil { // We don't return this error as it would override the actual response _ = cb.config.OnClose(c) } } return err } } ================================================ FILE: v3/circuitbreaker/circuitbreaker_test.go ================================================ package circuitbreaker import ( "encoding/json" "errors" "io" "net/http/httptest" "sync" "sync/atomic" "testing" "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" ) // mockTime helps control time for deterministic testing type mockTime struct { mu sync.Mutex current time.Time } func newMockTime(t time.Time) *mockTime { return &mockTime{current: t} } func (m *mockTime) Now() time.Time { m.mu.Lock() defer m.mu.Unlock() return m.current } func (m *mockTime) Add(d time.Duration) { m.mu.Lock() defer m.mu.Unlock() m.current = m.current.Add(d) } // TestCircuitBreakerStates tests each state transition of the circuit breaker func TestCircuitBreakerStates(t *testing.T) { mockClock := newMockTime(time.Now()) // Create circuit breaker with test config cb := New(Config{ FailureThreshold: 2, Timeout: 5 * time.Second, SuccessThreshold: 2, HalfOpenMaxConcurrent: 1, }) // Override the time function cb.now = mockClock.Now // Test initial state t.Run("Initial State", func(t *testing.T) { require.Equal(t, StateClosed, cb.GetState()) allowed, state := cb.AllowRequest() require.True(t, allowed) require.Equal(t, StateClosed, state) }) // Test transition to open state t.Run("Transition to Open", func(t *testing.T) { // Report failures to trip the circuit cb.ReportFailure() require.Equal(t, StateClosed, cb.GetState()) cb.ReportFailure() // This should trip the circuit require.Equal(t, StateOpen, cb.GetState()) allowed, state := cb.AllowRequest() require.False(t, allowed) require.Equal(t, StateOpen, state) }) // Test transition to half-open state t.Run("Transition to HalfOpen", func(t *testing.T) { // Advance time past the timeout to trigger half-open mockClock.Add(6 * time.Second) // Force timer activation by checking state // (In real usage this would happen automatically with timer) if cb.openTimer != nil { cb.openTimer.Stop() cb.transitionToHalfOpen() } require.Equal(t, StateHalfOpen, cb.GetState()) allowed, state := cb.AllowRequest() require.True(t, allowed) require.Equal(t, StateHalfOpen, state) // Release the semaphore for next test cb.ReleaseSemaphore() }) // Test half-open limited concurrency t.Run("HalfOpen Limited Concurrency", func(t *testing.T) { // Try to allow two concurrent requests when only one is permitted allowed1, _ := cb.AllowRequest() allowed2, _ := cb.AllowRequest() require.True(t, allowed1) require.False(t, allowed2) // Release the semaphore cb.ReleaseSemaphore() }) // Test transition back to open on failure in half-open t.Run("HalfOpen to Open on Failure", func(t *testing.T) { allowed, _ := cb.AllowRequest() require.True(t, allowed) cb.ReportFailure() require.Equal(t, StateOpen, cb.GetState()) // Even though we took a semaphore, it should be cleared by state transition allowed, _ = cb.AllowRequest() require.False(t, allowed) }) // Test transition to half-open again t.Run("Back to HalfOpen", func(t *testing.T) { mockClock.Add(6 * time.Second) // Force timer activation if cb.openTimer != nil { cb.openTimer.Stop() cb.transitionToHalfOpen() } require.Equal(t, StateHalfOpen, cb.GetState()) }) // Test transition to closed state t.Run("Transition to Closed", func(t *testing.T) { allowed, _ := cb.AllowRequest() require.True(t, allowed) cb.ReportSuccess() require.Equal(t, StateHalfOpen, cb.GetState()) cb.ReleaseSemaphore() allowed, _ = cb.AllowRequest() require.True(t, allowed) cb.ReportSuccess() // This should close the circuit require.Equal(t, StateClosed, cb.GetState()) cb.ReleaseSemaphore() }) // Test proper cleanup t.Run("Cleanup", func(t *testing.T) { cb.Stop() }) } // TestCircuitBreakerCallbacks tests the callback functions func TestCircuitBreakerCallbacks(t *testing.T) { var ( openCalled bool halfOpenCalled bool closedCalled bool ) cb := New(Config{ FailureThreshold: 2, Timeout: 1 * time.Millisecond, // Short timeout for quick tests SuccessThreshold: 1, HalfOpenMaxConcurrent: 1, OnOpen: func(c fiber.Ctx) error { openCalled = true return c.SendStatus(fiber.StatusServiceUnavailable) }, OnHalfOpen: func(c fiber.Ctx) error { halfOpenCalled = true return c.SendStatus(fiber.StatusTooManyRequests) }, OnClose: func(c fiber.Ctx) error { closedCalled = true return c.Next() }, }) app := fiber.New() app.Use(Middleware(cb)) app.Get("/test", func(c fiber.Ctx) error { return c.SendString("OK") }) // Test OnOpen callback t.Run("OnOpen Callback", func(t *testing.T) { // Trip the circuit cb.ReportFailure() cb.ReportFailure() // Request should be rejected req := httptest.NewRequest("GET", "/test", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode) require.True(t, openCalled) }) // Test OnHalfOpen callback t.Run("OnHalfOpen Callback", func(t *testing.T) { cb.transitionToHalfOpen() // Acquire the one allowed request allowed, state := cb.AllowRequest() require.True(t, allowed) require.Equal(t, StateHalfOpen, state) // Second request should be rejected with OnHalfOpen callback req := httptest.NewRequest("GET", "/test", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode) require.True(t, halfOpenCalled) // Release the semaphore cb.ReleaseSemaphore() }) // Test OnClose callback t.Run("OnClose Callback", func(t *testing.T) { // Reset for clean test closedCalled = false // Get to half-open state cb.transitionToHalfOpen() // Create a test request req := httptest.NewRequest("GET", "/test", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) require.True(t, closedCalled) // OnClose should be called after successful request }) // Clean up cb.Stop() } // TestMiddleware tests the middleware functionality func TestMiddleware(t *testing.T) { customErr := errors.New("custom error") cb := New(Config{ FailureThreshold: 2, Timeout: 5 * time.Second, SuccessThreshold: 2, IsFailure: func(c fiber.Ctx, err error) bool { // Count as failure if status >= 400 or has error return err != nil || c.Response().StatusCode() >= 400 }, }) app := fiber.New() app.Use(Middleware(cb)) // Success handler app.Get("/success", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) // Client error handler - 400 series app.Get("/client-error", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusBadRequest) }) // Server error handler - 500 series app.Get("/server-error", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) }) // Error handler app.Get("/error", func(c fiber.Ctx) error { return customErr }) t.Run("Successful Request", func(t *testing.T) { req := httptest.NewRequest("GET", "/success", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) }) t.Run("Client Error Counts as Failure", func(t *testing.T) { // Reset to closed state cb.transitionToClosed() // Send client error requests req := httptest.NewRequest("GET", "/client-error", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusBadRequest, resp.StatusCode) // Should increment failure count - check state remains closed require.Equal(t, StateClosed, cb.GetState()) // Second failure should trip circuit resp, err = app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusBadRequest, resp.StatusCode) // Circuit should now be open require.Equal(t, StateOpen, cb.GetState()) }) t.Run("Circuit Open Rejects Requests", func(t *testing.T) { // Circuit should be open from previous test req := httptest.NewRequest("GET", "/success", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode) }) // Clean up cb.Stop() } // TestConcurrentAccess tests the circuit breaker under concurrent load func TestConcurrentAccess(t *testing.T) { cb := New(Config{ FailureThreshold: 5, Timeout: 100 * time.Millisecond, SuccessThreshold: 3, HalfOpenMaxConcurrent: 2, }) t.Run("Concurrent Failures", func(t *testing.T) { var wg sync.WaitGroup // Simulate 10 goroutines reporting failures for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() cb.ReportFailure() }() } wg.Wait() // Circuit should be open after enough failures require.Equal(t, StateOpen, cb.GetState()) }) t.Run("Concurrent Half-Open Requests", func(t *testing.T) { // Force transition to half-open cb.transitionToHalfOpen() var wg sync.WaitGroup requestAllowed := make(chan bool, 10) // Try 10 concurrent requests for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() allowed, _ := cb.AllowRequest() requestAllowed <- allowed if allowed { // Simulate request processing time.Sleep(10 * time.Millisecond) cb.ReleaseSemaphore() } }() } wg.Wait() close(requestAllowed) // Count allowed requests allowedCount := 0 for allowed := range requestAllowed { if allowed { allowedCount++ } } // Only HalfOpenMaxConcurrent (2) requests should be allowed require.Equal(t, cb.config.HalfOpenMaxConcurrent, allowedCount) }) t.Run("Concurrent Successes to Close Circuit", func(t *testing.T) { // Force transition to half-open cb.transitionToHalfOpen() var wg sync.WaitGroup // Simulate 10 goroutines reporting successes for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() cb.ReportSuccess() }() } wg.Wait() // Circuit should be closed after enough successes require.Equal(t, StateClosed, cb.GetState()) }) // Clean up cb.Stop() } // TestCustomFailureDetection tests the custom failure detection logic func TestCustomFailureDetection(t *testing.T) { customFailureDetection := false cb := New(Config{ FailureThreshold: 1, IsFailure: func(c fiber.Ctx, err error) bool { // Custom logic: mark as failure only if our flag is set return customFailureDetection }, }) app := fiber.New() app.Use(Middleware(cb)) app.Get("/test", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) t.Run("Custom Success Logic", func(t *testing.T) { customFailureDetection = false // Even 500 status should be success with our custom logic app.Get("/server-error", func(c fiber.Ctx) error { c.Status(500) return nil }) req := httptest.NewRequest("GET", "/server-error", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, 500, resp.StatusCode) // Circuit should remain closed require.Equal(t, StateClosed, cb.GetState()) }) t.Run("Custom Failure Logic", func(t *testing.T) { customFailureDetection = true // Now even 200 status should be failure with our custom logic req := httptest.NewRequest("GET", "/test", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) // Circuit should be open require.Equal(t, StateOpen, cb.GetState()) }) // Clean up cb.Stop() } // TestHalfOpenConcurrencyConfig tests that HalfOpenMaxConcurrent setting works func TestHalfOpenConcurrencyConfig(t *testing.T) { // Create circuit breaker with 3 concurrent requests in half-open cb := New(Config{ FailureThreshold: 2, Timeout: 5 * time.Second, SuccessThreshold: 2, HalfOpenMaxConcurrent: 3, }) // Put circuit in half-open state cb.transitionToOpen() cb.transitionToHalfOpen() // Try to get more than allowed concurrent requests allowed1, _ := cb.AllowRequest() allowed2, _ := cb.AllowRequest() allowed3, _ := cb.AllowRequest() allowed4, _ := cb.AllowRequest() require.True(t, allowed1) require.True(t, allowed2) require.True(t, allowed3) require.False(t, allowed4) // Release all permits cb.ReleaseSemaphore() cb.ReleaseSemaphore() cb.ReleaseSemaphore() // Clean up cb.Stop() } // TestCircuitBreakerReset tests the Reset method func TestCircuitBreakerReset(t *testing.T) { mockClock := newMockTime(time.Now()) cb := New(Config{ FailureThreshold: 2, Timeout: 5 * time.Second, SuccessThreshold: 2, HalfOpenMaxConcurrent: 1, }) cb.now = mockClock.Now t.Run("Reset From Open State", func(t *testing.T) { // Put circuit in open state cb.ReportFailure() cb.ReportFailure() require.Equal(t, StateOpen, cb.GetState()) // Reset the circuit cb.Reset() // Verify state and counters require.Equal(t, StateClosed, cb.GetState()) require.Equal(t, int64(0), atomic.LoadInt64(&cb.failureCount)) require.Equal(t, int64(0), atomic.LoadInt64(&cb.successCount)) }) t.Run("Reset From HalfOpen State", func(t *testing.T) { // Put circuit in half-open state cb.ReportFailure() cb.ReportFailure() cb.transitionToHalfOpen() require.Equal(t, StateHalfOpen, cb.GetState()) // Take a semaphore allowed, _ := cb.AllowRequest() require.True(t, allowed) // Reset the circuit cb.Reset() // Verify state and that new requests are allowed require.Equal(t, StateClosed, cb.GetState()) allowed, _ = cb.AllowRequest() require.True(t, allowed) }) t.Run("Reset With Active Timer", func(t *testing.T) { // Put circuit in open state with active timer cb.ReportFailure() cb.ReportFailure() require.Equal(t, StateOpen, cb.GetState()) // Reset before timer expires cb.Reset() // Advance time past original timeout mockClock.Add(6 * time.Second) // Verify circuit remains closed require.Equal(t, StateClosed, cb.GetState()) }) t.Run("Reset Updates LastStateChange", func(t *testing.T) { initialTime := cb.lastStateChange // Wait a moment mockClock.Add(1 * time.Second) // Reset the circuit cb.Reset() // Verify lastStateChange was updated require.True(t, cb.lastStateChange.After(initialTime)) }) // Clean up cb.Stop() } // TestCircuitBreakerForceOpen tests the ForceOpen method func TestForceOpen(t *testing.T) { mockClock := newMockTime(time.Now()) cb := New(Config{ FailureThreshold: 2, Timeout: 5 * time.Second, SuccessThreshold: 2, HalfOpenMaxConcurrent: 1, }) cb.now = mockClock.Now t.Run("Force Open From Closed State", func(t *testing.T) { require.Equal(t, StateClosed, cb.GetState()) cb.ForceOpen() require.Equal(t, StateOpen, cb.GetState()) // Verify requests are rejected allowed, state := cb.AllowRequest() require.False(t, allowed) require.Equal(t, StateOpen, state) }) t.Run("Force Open From HalfOpen State", func(t *testing.T) { // First get to half-open state cb.transitionToOpen() cb.transitionToHalfOpen() require.Equal(t, StateHalfOpen, cb.GetState()) // Take a semaphore allowed, _ := cb.AllowRequest() require.True(t, allowed) // Force open should clear semaphore cb.ForceOpen() require.Equal(t, StateOpen, cb.GetState()) // Verify new requests are rejected allowed, _ = cb.AllowRequest() require.False(t, allowed) }) t.Run("Force Open With Active Timer", func(t *testing.T) { cb.transitionToClosed() cb.ForceOpen() // Advance time past timeout mockClock.Add(6 * time.Second) // Should still be open since ForceOpen overrides normal timeout require.Equal(t, StateOpen, cb.GetState()) }) t.Run("Force Open Multiple Times", func(t *testing.T) { // Multiple force open calls should maintain open state cb.ForceOpen() cb.ForceOpen() require.Equal(t, StateOpen, cb.GetState()) // Verify counters are reset each time require.Equal(t, int64(0), atomic.LoadInt64(&cb.failureCount)) require.Equal(t, int64(0), atomic.LoadInt64(&cb.successCount)) }) // Clean up cb.Stop() } // TestHealthHandler tests the health check endpoint handler func TestHealthHandler(t *testing.T) { cb := New(Config{ FailureThreshold: 2, Timeout: 5 * time.Second, SuccessThreshold: 2, HalfOpenMaxConcurrent: 1, }) app := fiber.New() app.Get("/health", cb.HealthHandler()) t.Run("Healthy When Closed", func(t *testing.T) { cb.transitionToClosed() req := httptest.NewRequest("GET", "/health", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) var result fiber.Map body, err := io.ReadAll(resp.Body) require.NoError(t, err) err = json.Unmarshal(body, &result) require.NoError(t, err) require.Equal(t, string(StateClosed), result["state"]) require.Equal(t, true, result["healthy"]) }) t.Run("Unhealthy When Open", func(t *testing.T) { cb.transitionToOpen() req := httptest.NewRequest("GET", "/health", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode) var result fiber.Map body, err := io.ReadAll(resp.Body) require.NoError(t, err) err = json.Unmarshal(body, &result) require.NoError(t, err) require.Equal(t, string(StateOpen), result["state"]) require.Equal(t, false, result["healthy"]) }) t.Run("Response Content Type", func(t *testing.T) { req := httptest.NewRequest("GET", "/health", nil) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type")) }) // Clean up cb.Stop() } ================================================ FILE: v3/circuitbreaker/go.mod ================================================ module github.com/gofiber/contrib/v3/circuitbreaker go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/circuitbreaker/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/coraza/README.md ================================================ --- id: coraza --- # Coraza ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*coraza*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20Coraza/badge.svg) [Coraza](https://coraza.io/) WAF middleware for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get github.com/gofiber/fiber/v3 go get github.com/gofiber/contrib/v3/coraza ``` ## Signature ```go coraza.New(config ...coraza.Config) fiber.Handler coraza.NewEngine(config coraza.Config) (*coraza.Engine, error) ``` ## Config | Property | Type | Description | Default | |:--|:--|:--|:--| | Next | `func(fiber.Ctx) bool` | Defines a function to skip this middleware when it returns true | `nil` | | BlockHandler | `func(fiber.Ctx, coraza.InterruptionDetails) error` | Custom handler for blocked requests | `nil` | | ErrorHandler | `func(fiber.Ctx, coraza.MiddlewareError) error` | Custom handler for middleware failures | `nil` | | DirectivesFile | `[]string` | Coraza directives files loaded in order | `nil` | | RootFS | `fs.FS` | Optional filesystem used to resolve `DirectivesFile` | `nil` | | BlockMessage | `string` | Message returned by the built-in block handler | `"Request blocked by Web Application Firewall"` | | LogLevel | `fiberlog.Level` | Middleware lifecycle log level | `fiberlog.LevelInfo` in `coraza.ConfigDefault` | | RequestBodyAccess | `bool` | Enables request body inspection | `true` in `coraza.ConfigDefault` | | MetricsCollector | `coraza.MetricsCollector` | Optional custom in-memory metrics collector | `nil` (falls back to the built-in collector) | If you want the defaults, start from `coraza.ConfigDefault` and override the fields you need. For 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. By default, the middleware starts without external rule files. Set `DirectivesFile` to load your Coraza or CRS ruleset. Request body size follows the Fiber app `BodyLimit`. Wildcard 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. ## Usage ```go package main import ( "log" "github.com/gofiber/contrib/v3/coraza" "github.com/gofiber/fiber/v3" ) func main() { app := fiber.New() cfg := coraza.ConfigDefault cfg.DirectivesFile = []string{"./conf/coraza.conf"} app.Use(coraza.New(cfg)) app.Get("/", func(c fiber.Ctx) error { return c.SendString("ok") }) log.Fatal(app.Listen(":3000")) } ``` ## Advanced usage with Engine Use `NewEngine` when you need explicit lifecycle control, reload support, or observability data. ```go engineCfg := coraza.ConfigDefault engineCfg.DirectivesFile = []string{"./conf/coraza.conf"} engine, err := coraza.NewEngine(engineCfg) if err != nil { log.Fatal(err) } app.Use(engine.Middleware(coraza.MiddlewareConfig{ Next: func(c fiber.Ctx) bool { return c.Path() == "/healthz" }, BlockHandler: func(c fiber.Ctx, details coraza.InterruptionDetails) error { return c.Status(details.StatusCode).JSON(fiber.Map{ "blocked": true, "rule_id": details.RuleID, }) }, })) ``` ## Engine observability The middleware does not open operational routes for you, but `Engine` exposes data-oriented methods that can be used to build your own endpoints: - `engine.Reload()` - `engine.MetricsSnapshot()` - `engine.Snapshot()` - `engine.Report()` ## Notes - Request headers and request bodies are inspected. - Request body size follows the Fiber app `BodyLimit`. - Response body inspection is not supported. - `coraza.New()` starts successfully without external rule files, but it does not load any rules until `DirectivesFile` is configured. - Invalid configuration causes `coraza.New(...)` to panic during startup, which allows applications to fail fast. ## References - [Coraza Docs](https://coraza.io/) - [OWASP Core Rule Set](https://coraza.io/docs/tutorials/coreruleset) ================================================ FILE: v3/coraza/coraza.go ================================================ // Package coraza provides Coraza WAF middleware for Fiber. package coraza import ( "fmt" "io" "io/fs" "net" "net/http" "os" "path/filepath" "reflect" "strconv" "strings" "sync" "time" "github.com/corazawaf/coraza/v3" "github.com/corazawaf/coraza/v3/experimental" "github.com/corazawaf/coraza/v3/types" "github.com/gofiber/fiber/v3" fiberlog "github.com/gofiber/fiber/v3/log" "github.com/gofiber/fiber/v3/middleware/adaptor" ) const defaultBlockMessage = "Request blocked by Web Application Firewall" // Config defines the configuration for the Coraza middleware and Engine. // // For zero-value-backed fields such as RequestBodyAccess=false, // LogLevel=fiberlog.LevelTrace, or resetting MetricsCollector to the built-in // default, start from ConfigDefault or use the WithRequestBodyAccess, // WithLogLevel, and WithMetricsCollector helpers so the override remains // explicit. type Config struct { // Next defines a function to skip this middleware when it returns true. Next func(fiber.Ctx) bool // BlockHandler customizes the response returned for interrupted requests. BlockHandler BlockHandler // ErrorHandler customizes the response returned for middleware failures. ErrorHandler ErrorHandler // DirectivesFile lists Coraza directives files to load in order. // When empty, the engine starts without external rule files. DirectivesFile []string // RootFS is an optional filesystem used to resolve DirectivesFile entries. RootFS fs.FS // BlockMessage overrides the message used by the built-in block handler. BlockMessage string // LogLevel controls middleware lifecycle logging. LogLevel fiberlog.Level // RequestBodyAccess enables request body inspection in Coraza. RequestBodyAccess bool // MetricsCollector overrides the default in-memory metrics collector. MetricsCollector MetricsCollector logLevelSet bool requestBodyAccessSet bool metricsCollectorSet bool } // ConfigDefault provides the default Coraza configuration. var ConfigDefault = Config{ LogLevel: fiberlog.LevelInfo, RequestBodyAccess: true, logLevelSet: true, requestBodyAccessSet: true, } // MiddlewareConfig customizes how Engine middleware behaves for a specific mount. type MiddlewareConfig struct { // Next bypasses WAF inspection when it returns true. Next func(fiber.Ctx) bool // BlockHandler customizes the response returned for interrupted requests. BlockHandler BlockHandler // ErrorHandler customizes the response returned for middleware failures. ErrorHandler ErrorHandler } // MiddlewareError describes an operational failure that occurred while handling a request. type MiddlewareError struct { // StatusCode is the HTTP status code suggested for the failure response. StatusCode int // Code is a stable application-level error code for the failure type. Code string // Message is the client-facing error message. Message string // Err is the underlying error when one is available. Err error } // InterruptionDetails describes a Coraza interruption returned by request inspection. type InterruptionDetails struct { // StatusCode is the HTTP status code associated with the interruption. StatusCode int // Action is the Coraza action, such as "deny". Action string // RuleID is the matched Coraza rule identifier when available. RuleID int // Data contains rule-specific interruption data when available. Data string // Message is the message returned by the built-in block handler. Message string } // BlockHandler handles requests that were interrupted by the WAF. type BlockHandler func(fiber.Ctx, InterruptionDetails) error // ErrorHandler handles middleware errors that prevented request inspection. type ErrorHandler func(fiber.Ctx, MiddlewareError) error // Engine owns a Coraza WAF instance and exposes Fiber middleware around it. type Engine struct { mu sync.RWMutex waf coraza.WAF wafWithOptions experimental.WAFWithOptions supportsOptions bool initErr error activeCfg Config lastAttemptCfg Config blockMessage string logLevel fiberlog.Level metrics MetricsCollector reloadCount uint64 lastLoadedAt time.Time initSuccessCount uint64 initFailureCount uint64 reloadSuccessCount uint64 reloadFailureCount uint64 } // New constructs Coraza Fiber middleware. // // It panics if the provided configuration cannot initialize a WAF instance. func New(config ...Config) fiber.Handler { cfg := ConfigDefault if len(config) > 0 { cfg = resolveConfig(config[0]) } engine, err := NewEngine(cfg) if err != nil { panic(err) } return engine.Middleware(MiddlewareConfig{ Next: cfg.Next, BlockHandler: cfg.BlockHandler, ErrorHandler: cfg.ErrorHandler, }) } // NewEngine creates and initializes an Engine with the provided configuration. func NewEngine(cfg Config) (*Engine, error) { engine := newEngine(nil) if err := engine.Init(cfg); err != nil { return nil, err } return engine, nil } // Init replaces the Engine's WAF instance using the provided configuration. // // On failure, the last working WAF instance is kept in place and the failure is // recorded for observability. func (e *Engine) Init(cfg Config) error { resolvedCfg := resolveConfig(cfg) metrics := resolveMetricsCollector(resolvedCfg.MetricsCollector) newWAF, err := createWAFWithConfig(resolvedCfg) logLevel := normalizeLogLevel(resolvedCfg.LogLevel) e.mu.Lock() defer e.mu.Unlock() e.lastAttemptCfg = cloneConfig(resolvedCfg) if err != nil { e.initErr = err e.initFailureCount++ logWithLevel(logLevel, fiberlog.LevelError, "Coraza initialization failed", "error", err.Error()) return err } e.waf = newWAF e.initErr = nil e.setWAFOptionsStateLocked(newWAF) e.activeCfg = cloneConfig(resolvedCfg) e.lastLoadedAt = time.Now() e.initSuccessCount++ e.blockMessage = resolveBlockMessage(resolvedCfg.BlockMessage) e.logLevel = logLevel e.metrics = metrics logWithLevel(logLevel, fiberlog.LevelInfo, "Coraza initialized successfully", "supports_options", e.supportsOptions) return nil } // SetBlockMessage overrides the default message returned by the built-in block handler. func (e *Engine) SetBlockMessage(msg string) { e.mu.Lock() defer e.mu.Unlock() e.blockMessage = resolveBlockMessage(msg) } // Metrics returns the Engine's metrics collector. func (e *Engine) Metrics() MetricsCollector { e.mu.RLock() defer e.mu.RUnlock() return e.metrics } // Middleware creates a Fiber middleware handler backed by the Engine's WAF instance. func (e *Engine) Middleware(config ...MiddlewareConfig) fiber.Handler { mwCfg := MiddlewareConfig{} if len(config) > 0 { mwCfg = config[0] } return func(c fiber.Ctx) error { if mwCfg.Next != nil && mwCfg.Next(c) { return c.Next() } startTime := time.Now() metrics := e.Metrics() metrics.RecordRequest() defer func() { metrics.RecordLatency(time.Since(startTime)) }() currentWAF, currentSupportsOptions, currentWAFWithOptions, currentErr := e.snapshot() if currentWAF == nil { if currentErr != nil { return e.handleError(c, mwCfg, MiddlewareError{ StatusCode: http.StatusInternalServerError, Code: "waf_init_failed", Message: "WAF initialization failed", Err: currentErr, }) } return e.handleError(c, mwCfg, MiddlewareError{ StatusCode: http.StatusInternalServerError, Code: "waf_not_initialized", Message: "WAF instance not initialized", }) } it, mwErr := e.inspectRequest(c, currentWAF, currentSupportsOptions, currentWAFWithOptions) if mwErr != nil { return e.handleError(c, mwCfg, *mwErr) } if it != nil { metrics.RecordBlock() details := InterruptionDetails{ StatusCode: obtainStatusCodeFromInterruptionOrDefault(it, http.StatusForbidden), Action: it.Action, RuleID: it.RuleID, Data: it.Data, Message: e.blockMessageValue(), } e.log(fiberlog.LevelWarn, "Coraza request interrupted", "rule_id", details.RuleID, "action", details.Action, "status", details.StatusCode) if mwCfg.BlockHandler != nil { return mwCfg.BlockHandler(c, details) } return defaultBlockHandler(c, details) } return c.Next() } } func (e *Engine) inspectRequest( c fiber.Ctx, currentWAF coraza.WAF, currentSupportsOptions bool, currentWAFWithOptions experimental.WAFWithOptions, ) (_ *types.Interruption, mwErr *MiddlewareError) { var tx types.Transaction defer func() { if r := recover(); r != nil { e.log(fiberlog.LevelError, "Coraza panic recovered", "panic", r, "method", c.Method(), "path", c.Path(), "ip", c.IP()) mwErr = &MiddlewareError{ StatusCode: http.StatusInternalServerError, Code: "waf_panic_recovered", Message: "WAF internal error", Err: fmt.Errorf("panic recovered: %v", r), } } if tx != nil { e.finishTransaction(c, tx, &mwErr) } }() stdReq, err := convertFiberToStdRequest(c) if err != nil { return nil, &MiddlewareError{ StatusCode: http.StatusInternalServerError, Code: "waf_request_convert_failed", Message: "Failed to convert request", Err: err, } } if currentSupportsOptions && currentWAFWithOptions != nil { tx = currentWAFWithOptions.NewTransactionWithOptions(experimental.Options{ Context: stdReq.Context(), }) } else { tx = currentWAF.NewTransaction() } if tx.IsRuleEngineOff() { return nil, nil } it, err := processRequest(tx, stdReq, c.App().Config().BodyLimit) if err != nil { return nil, &MiddlewareError{ StatusCode: http.StatusInternalServerError, Code: "waf_request_processing_failed", Message: "WAF request processing failed", Err: err, } } return it, nil } func (e *Engine) finishTransaction(c fiber.Ctx, tx types.Transaction, mwErr **MiddlewareError) { defer func() { if r := recover(); r != nil { e.log(fiberlog.LevelError, "Coraza cleanup panic recovered", "panic", r, "method", c.Method(), "path", c.Path(), "ip", c.IP()) if *mwErr == nil { *mwErr = &MiddlewareError{ StatusCode: http.StatusInternalServerError, Code: "waf_cleanup_panic_recovered", Message: "WAF internal error", Err: fmt.Errorf("cleanup panic recovered: %v", r), } } } }() tx.ProcessLogging() if err := tx.Close(); err != nil { e.log(fiberlog.LevelDebug, "Coraza transaction close failed", "error", err.Error()) } } // Reload rebuilds the current WAF instance using the active configuration. func (e *Engine) Reload() error { e.mu.RLock() cfg := cloneConfig(e.activeCfg) e.mu.RUnlock() logLevel := normalizeLogLevel(cfg.LogLevel) logWithLevel(logLevel, fiberlog.LevelInfo, "Coraza starting manual reload") newWAF, err := createWAFWithConfig(cfg) if err != nil { e.mu.Lock() e.reloadFailureCount++ e.mu.Unlock() logWithLevel(logLevel, fiberlog.LevelError, "Coraza reload failed", "error", err.Error()) return fmt.Errorf("failed to reload WAF: %w", err) } e.mu.Lock() e.waf = newWAF e.initErr = nil e.setWAFOptionsStateLocked(newWAF) e.reloadCount++ e.reloadSuccessCount++ e.lastLoadedAt = time.Now() reloadCount := e.reloadCount e.logLevel = logLevel e.mu.Unlock() logWithLevel(logLevel, fiberlog.LevelInfo, "Coraza reload completed successfully", "reload_count", reloadCount) return nil } func (e *Engine) snapshot() (coraza.WAF, bool, experimental.WAFWithOptions, error) { e.mu.RLock() defer e.mu.RUnlock() return e.waf, e.supportsOptions, e.wafWithOptions, e.initErr } func (e *Engine) setWAFOptionsStateLocked(waf coraza.WAF) { if wafWithOptions, ok := waf.(experimental.WAFWithOptions); ok { e.wafWithOptions = wafWithOptions e.supportsOptions = true return } e.wafWithOptions = nil e.supportsOptions = false } func (e *Engine) blockMessageValue() string { e.mu.RLock() defer e.mu.RUnlock() return e.blockMessage } func (e *Engine) handleError(c fiber.Ctx, cfg MiddlewareConfig, mwErr MiddlewareError) error { if cfg.ErrorHandler != nil { return cfg.ErrorHandler(c, mwErr) } return defaultErrorHandler(c, mwErr) } func newEngine(collector MetricsCollector) *Engine { return &Engine{ blockMessage: defaultBlockMessage, logLevel: fiberlog.LevelInfo, metrics: resolveMetricsCollector(collector), } } func isNilMetricsCollector(collector MetricsCollector) bool { if collector == nil { return true } value := reflect.ValueOf(collector) switch value.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return value.IsNil() default: return false } } func resolveMetricsCollector(collector MetricsCollector) MetricsCollector { if isNilMetricsCollector(collector) { return NewDefaultMetricsCollector() } return collector } func (e *Engine) observabilitySnapshot() EngineSnapshot { e.mu.RLock() defer e.mu.RUnlock() var lastInitError string if e.initErr != nil { lastInitError = e.initErr.Error() } configFiles := append([]string(nil), e.activeCfg.DirectivesFile...) lastAttemptConfigFiles := append([]string(nil), e.lastAttemptCfg.DirectivesFile...) return EngineSnapshot{ Initialized: e.waf != nil, SupportsOptions: e.supportsOptions, ConfigFiles: configFiles, LastAttemptConfigFiles: lastAttemptConfigFiles, LastInitError: lastInitError, LastLoadedAt: e.lastLoadedAt, InitSuccessTotal: e.initSuccessCount, InitFailureTotal: e.initFailureCount, ReloadSuccessTotal: e.reloadSuccessCount, ReloadFailureTotal: e.reloadFailureCount, ReloadCount: e.reloadCount, } } func defaultBlockHandler(c fiber.Ctx, details InterruptionDetails) error { c.Set("X-WAF-Blocked", "true") return fiber.NewError(details.StatusCode, details.Message) } func defaultErrorHandler(_ fiber.Ctx, mwErr MiddlewareError) error { return fiber.NewError(mwErr.StatusCode, mwErr.Message) } func processRequest(tx types.Transaction, req *http.Request, bodyLimit int) (*types.Interruption, error) { client, cport := splitRemoteAddr(req.RemoteAddr) tx.ProcessConnection(client, cport, "", 0) tx.ProcessURI(req.URL.String(), req.Method, req.Proto) for k, values := range req.Header { for _, v := range values { tx.AddRequestHeader(k, v) } } if req.Host != "" { tx.AddRequestHeader("Host", req.Host) tx.SetServerName(req.Host) } for _, te := range req.TransferEncoding { tx.AddRequestHeader("Transfer-Encoding", te) } if in := tx.ProcessRequestHeaders(); in != nil { return in, nil } if tx.IsRequestBodyAccessible() && req.Body != nil && req.Body != http.NoBody { bodyReader := io.Reader(req.Body) if bodyLimit > 0 { bodyReader = io.LimitReader(req.Body, int64(bodyLimit)) } it, _, err := tx.ReadRequestBodyFrom(bodyReader) if err != nil { return nil, err } if it != nil { return it, nil } } return tx.ProcessRequestBody() } func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultStatusCode int) int { if it.Action == "deny" { if it.Status != 0 { return it.Status } return http.StatusForbidden } return defaultStatusCode } func convertFiberToStdRequest(c fiber.Ctx) (*http.Request, error) { req, err := adaptor.ConvertRequest(c, false) if err != nil { return nil, err } req.RemoteAddr = net.JoinHostPort(c.IP(), c.Port()) if req.Host == "" { req.Host = c.Hostname() } return req, nil } func createWAFWithConfig(cfg Config) (coraza.WAF, error) { var directivesFiles []string logLevel := normalizeLogLevel(cfg.LogLevel) for _, path := range cfg.DirectivesFile { expandedPaths, err := resolveDirectivesFiles(cfg.RootFS, path, logLevel) if err != nil { return nil, err } directivesFiles = append(directivesFiles, expandedPaths...) } wafConfig := coraza.NewWAFConfig() if cfg.RequestBodyAccess { wafConfig = wafConfig.WithRequestBodyAccess() } if cfg.RootFS != nil { wafConfig = wafConfig.WithRootFS(cfg.RootFS) } for _, path := range directivesFiles { wafConfig = wafConfig.WithDirectivesFromFile(path) } return coraza.NewWAF(wafConfig) } func resolveDirectivesFiles(root fs.FS, path string, logLevel fiberlog.Level) ([]string, error) { if strings.ContainsAny(path, "*?[") { logWithLevel(logLevel, fiberlog.LevelWarn, "Coraza directives path uses glob matching and is expanded before initialization", "path", path, "note", "all matching directives files will be loaded in sorted order", ) var ( matches []string err error ) if root != nil { matches, err = fs.Glob(root, path) } else { matches, err = filepath.Glob(path) } if err != nil { return nil, fmt.Errorf("invalid Coraza directives glob %q: %w", path, err) } if len(matches) == 0 { return nil, fmt.Errorf("coraza directives glob %q matched no files", path) } return matches, nil } if root != nil { if _, err := fs.Stat(root, path); err != nil { return nil, fmt.Errorf("coraza directives file %q not found in RootFS: %w", path, err) } return []string{path}, nil } if _, err := os.Stat(path); err != nil { return nil, fmt.Errorf("coraza directives file %q not found: %w", path, err) } return []string{path}, nil } func splitRemoteAddr(remoteAddr string) (string, int) { host, port, err := net.SplitHostPort(remoteAddr) if err != nil { return remoteAddr, 0 } portNum, err := strconv.Atoi(port) if err != nil { return host, 0 } return host, portNum } func cloneConfig(cfg Config) Config { clone := cfg clone.DirectivesFile = append([]string(nil), cfg.DirectivesFile...) return clone } func resolveConfig(cfg Config) Config { resolved := ConfigDefault if cfg.Next != nil { resolved.Next = cfg.Next } if cfg.BlockHandler != nil { resolved.BlockHandler = cfg.BlockHandler } if cfg.ErrorHandler != nil { resolved.ErrorHandler = cfg.ErrorHandler } if cfg.DirectivesFile != nil { resolved.DirectivesFile = append([]string(nil), cfg.DirectivesFile...) } if cfg.RootFS != nil { resolved.RootFS = cfg.RootFS } if cfg.BlockMessage != "" { resolved.BlockMessage = cfg.BlockMessage } if cfg.logLevelSet || cfg.LogLevel != 0 { resolved.LogLevel = normalizeLogLevel(cfg.LogLevel) } if cfg.requestBodyAccessSet || cfg.RequestBodyAccess { resolved.RequestBodyAccess = cfg.RequestBodyAccess } if cfg.metricsCollectorSet || !isNilMetricsCollector(cfg.MetricsCollector) { resolved.MetricsCollector = cfg.MetricsCollector } resolved.logLevelSet = true resolved.requestBodyAccessSet = true resolved.metricsCollectorSet = cfg.metricsCollectorSet || !isNilMetricsCollector(cfg.MetricsCollector) return resolved } // WithLogLevel returns a copy of cfg with an explicit lifecycle log level. func (cfg Config) WithLogLevel(level fiberlog.Level) Config { cfg.LogLevel = level cfg.logLevelSet = true return cfg } // WithRequestBodyAccess returns a copy of cfg with explicit request body inspection behavior. func (cfg Config) WithRequestBodyAccess(enabled bool) Config { cfg.RequestBodyAccess = enabled cfg.requestBodyAccessSet = true return cfg } // WithMetricsCollector returns a copy of cfg with an explicit metrics collector choice. func (cfg Config) WithMetricsCollector(collector MetricsCollector) Config { cfg.MetricsCollector = collector cfg.metricsCollectorSet = true return cfg } func resolveBlockMessage(msg string) string { if msg == "" { return defaultBlockMessage } return msg } func normalizeLogLevel(level fiberlog.Level) fiberlog.Level { switch level { case fiberlog.LevelTrace, fiberlog.LevelDebug, fiberlog.LevelInfo, fiberlog.LevelWarn, fiberlog.LevelError: return level default: return fiberlog.LevelInfo } } func logWithLevel(configLevel, targetLevel fiberlog.Level, msg string, keysAndValues ...any) { if normalizeLogLevel(configLevel) > normalizeLogLevel(targetLevel) { return } switch targetLevel { case fiberlog.LevelTrace: fiberlog.Tracew(msg, keysAndValues...) case fiberlog.LevelDebug: fiberlog.Debugw(msg, keysAndValues...) case fiberlog.LevelWarn: fiberlog.Warnw(msg, keysAndValues...) case fiberlog.LevelError: fiberlog.Errorw(msg, keysAndValues...) default: fiberlog.Infow(msg, keysAndValues...) } } func (e *Engine) currentLogLevel() fiberlog.Level { e.mu.RLock() defer e.mu.RUnlock() return e.logLevel } func (e *Engine) log(targetLevel fiberlog.Level, msg string, keysAndValues ...any) { logWithLevel(e.currentLogLevel(), targetLevel, msg, keysAndValues...) } ================================================ FILE: v3/coraza/coraza_test.go ================================================ package coraza import ( "bytes" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/corazawaf/coraza/v3/debuglog" "github.com/corazawaf/coraza/v3/types" "github.com/gofiber/fiber/v3" fiberlog "github.com/gofiber/fiber/v3/log" ) const testRules = `SecRuleEngine On SecRequestBodyAccess On SecRule ARGS:attack "@streq 1" "id:1001,phase:2,deny,status:403,msg:'attack detected'"` func TestNewPanicsOnInvalidConfig(t *testing.T) { defer func() { if r := recover(); r == nil { t.Fatal("expected New to panic when config is invalid") } }() _ = New(Config{DirectivesFile: []string{"missing.conf"}}) } func TestNewWithoutConfigReturnsMiddleware(t *testing.T) { app := fiber.New() app.Use(New()) app.Get("/", func(c fiber.Ctx) error { return c.SendString("ok") }) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/", nil)) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read response body: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected status 200, got %d", resp.StatusCode) } if string(body) != "ok" { t.Fatalf("expected body ok, got %q", string(body)) } } func TestNewEngineWithLocalFile(t *testing.T) { path := writeRuleFile(t, t.TempDir(), "local.conf", testRules) engine, err := NewEngine(Config{ LogLevel: fiberlog.LevelInfo, DirectivesFile: []string{path}, BlockMessage: "blocked from config", RequestBodyAccess: true, }) if err != nil { t.Fatalf("expected successful initialization, got error: %v", err) } if engine.initErr != nil { t.Fatalf("expected successful initialization, got error: %v", engine.initErr) } if engine.waf == nil { t.Fatal("expected engine WAF to be initialized") } if engine.blockMessage != "blocked from config" { t.Fatalf("expected block message to be initialized from config, got %q", engine.blockMessage) } } func TestSetBlockMessageEmptyResetsDefault(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } engine.SetBlockMessage("custom block") engine.SetBlockMessage("") if got := engine.blockMessageValue(); got != defaultBlockMessage { t.Fatalf("expected empty block message to restore default, got %q", got) } } func TestNewEngineWithRootFS(t *testing.T) { tempDir := t.TempDir() writeRuleFile(t, tempDir, "rootfs.conf", testRules) engine, err := NewEngine(Config{ DirectivesFile: []string{"rootfs.conf"}, RootFS: os.DirFS(tempDir), RequestBodyAccess: true, }) if err != nil { t.Fatalf("expected RootFS initialization to succeed, got error: %v", err) } if engine.initErr != nil { t.Fatalf("expected RootFS initialization to succeed, got error: %v", engine.initErr) } if engine.waf == nil { t.Fatal("expected engine WAF to be initialized from RootFS") } } func TestNewEngineMissingFile(t *testing.T) { _, err := NewEngine(Config{ DirectivesFile: []string{"missing.conf"}, }) if err == nil { t.Fatal("expected initialization to fail for missing directives file") } } func TestNewReturnsMiddleware(t *testing.T) { path := writeRuleFile(t, t.TempDir(), "test.conf", testRules) app := fiber.New() app.Use(New(Config{ DirectivesFile: []string{path}, RequestBodyAccess: true, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("ok") }) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?attack=1", nil)) defer resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected status 403, got %d", resp.StatusCode) } } func TestNewAppliesConfigDefaults(t *testing.T) { bodyRules := `SecRuleEngine On SecRequestBodyAccess On SecRule REQUEST_BODY "@contains attack" "id:1002,phase:2,deny,status:403,msg:'body attack detected'"` path := writeRuleFile(t, t.TempDir(), "body.conf", bodyRules) app := fiber.New() app.Use(New(Config{ DirectivesFile: []string{path}, })) app.Post("/", func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("payload=attack")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := performRequest(t, app, req) defer resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected status 403 when New applies default request body access, got %d", resp.StatusCode) } } func TestResolveConfigHonorsExplicitZeroValueOverrides(t *testing.T) { resolved := resolveConfig(Config{}.WithRequestBodyAccess(false).WithLogLevel(fiberlog.LevelTrace)) if resolved.RequestBodyAccess { t.Fatal("expected explicit request body access override to remain false") } if resolved.LogLevel != fiberlog.LevelTrace { t.Fatalf("expected explicit trace log level override, got %v", resolved.LogLevel) } } func TestEngineMiddlewareAllowsCleanRequest(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := newInstanceApp(engine, MiddlewareConfig{}) req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) resp := performRequest(t, app, req) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read response body: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("expected status 200, got %d", resp.StatusCode) } if string(body) != "ok" { t.Fatalf("expected body ok, got %q", string(body)) } metrics := engine.MetricsSnapshot() if metrics.TotalRequests != 1 || metrics.BlockedRequests != 0 { t.Fatalf("unexpected metrics after clean request: %+v", metrics) } } func TestEngineMiddlewareBlocksMaliciousRequest(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := newInstanceApp(engine, MiddlewareConfig{}) req := httptest.NewRequest(http.MethodGet, "/?attack=1", nil) resp := performRequest(t, app, req) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read response body: %v", err) } if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected status 403, got %d", resp.StatusCode) } if resp.Header.Get("X-WAF-Blocked") != "true" { t.Fatalf("expected X-WAF-Blocked header to be true, got %q", resp.Header.Get("X-WAF-Blocked")) } if !strings.Contains(string(body), defaultBlockMessage) { t.Fatalf("expected block message in response body, got %q", string(body)) } metrics := engine.MetricsSnapshot() if metrics.TotalRequests != 1 || metrics.BlockedRequests != 1 { t.Fatalf("unexpected metrics after blocked request: %+v", metrics) } } func TestEngineMiddlewareBlocksMaliciousRequestBody(t *testing.T) { bodyRules := `SecRuleEngine On SecRequestBodyAccess On SecRule REQUEST_BODY "@contains attack" "id:1002,phase:2,deny,status:403,msg:'body attack detected'"` engine, err := newTestEngineWithRules(t, bodyRules) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := newInstanceApp(engine, MiddlewareConfig{}) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("payload=attack")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp := performRequest(t, app, req) defer resp.Body.Close() if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected status 403 for malicious body, got %d", resp.StatusCode) } metrics := engine.MetricsSnapshot() if metrics.TotalRequests != 1 || metrics.BlockedRequests != 1 { t.Fatalf("unexpected metrics after blocked body request: %+v", metrics) } } func TestEngineMiddlewareRespectsFiberBodyLimit(t *testing.T) { bodyRules := `SecRuleEngine On SecRequestBodyAccess On SecRule REQUEST_BODY "@contains attack" "id:1002,phase:2,deny,status:403,msg:'body attack detected'"` engine, err := newTestEngineWithRules(t, bodyRules) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := fiber.New(fiber.Config{ BodyLimit: 8, }) app.Use(engine.Middleware()) app.Post("/", func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("payload=attack")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") _, err = app.Test(req) if err == nil { t.Fatal("expected Fiber body limit error, got nil") } if err.Error() != "body size exceeds the given limit" { t.Fatalf("expected Fiber body limit error, got %v", err) } } func TestNewEngineProvidesInstanceIsolation(t *testing.T) { first, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create first engine: %v", err) } second, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create second engine: %v", err) } first.SetBlockMessage("blocked by first") second.SetBlockMessage("blocked by second") firstApp := newInstanceApp(first, MiddlewareConfig{}) secondApp := newInstanceApp(second, MiddlewareConfig{}) firstResp := performRequest(t, firstApp, httptest.NewRequest(http.MethodGet, "/?attack=1", nil)) defer firstResp.Body.Close() firstBody, err := io.ReadAll(firstResp.Body) if err != nil { t.Fatalf("failed to read first response body: %v", err) } secondResp := performRequest(t, secondApp, httptest.NewRequest(http.MethodGet, "/?attack=1", nil)) defer secondResp.Body.Close() secondBody, err := io.ReadAll(secondResp.Body) if err != nil { t.Fatalf("failed to read second response body: %v", err) } if !strings.Contains(string(firstBody), "blocked by first") { t.Fatalf("expected first engine response to contain its block message, got %q", string(firstBody)) } if !strings.Contains(string(secondBody), "blocked by second") { t.Fatalf("expected second engine response to contain its block message, got %q", string(secondBody)) } } func TestMiddlewareConfigNextBypassesInspection(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := newInstanceApp(engine, MiddlewareConfig{ Next: func(c fiber.Ctx) bool { return c.Query("attack") == "1" }, }) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?attack=1", nil)) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected skipped request to pass through, got %d", resp.StatusCode) } metrics := engine.MetricsSnapshot() if metrics.TotalRequests != 0 || metrics.BlockedRequests != 0 { t.Fatalf("expected skipped request not to affect metrics, got %+v", metrics) } } func TestMiddlewareConfigCustomBlockHandler(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := newInstanceApp(engine, MiddlewareConfig{ BlockHandler: func(c fiber.Ctx, details InterruptionDetails) error { c.Set("X-Custom-Block", "true") return c.Status(http.StatusTeapot).JSON(fiber.Map{ "rule_id": details.RuleID, "status": details.StatusCode, }) }, }) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?attack=1", nil)) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read custom block response body: %v", err) } if resp.StatusCode != http.StatusTeapot { t.Fatalf("expected custom block status 418, got %d", resp.StatusCode) } if resp.Header.Get("X-Custom-Block") != "true" { t.Fatalf("expected custom block header, got %q", resp.Header.Get("X-Custom-Block")) } if !strings.Contains(string(body), `"rule_id":1001`) { t.Fatalf("expected custom block body to include rule id, got %q", string(body)) } } func TestMiddlewareConfigCustomErrorHandler(t *testing.T) { engine := newEngine(NewDefaultMetricsCollector()) app := newInstanceApp(engine, MiddlewareConfig{ ErrorHandler: func(c fiber.Ctx, mwErr MiddlewareError) error { return c.Status(http.StatusServiceUnavailable).JSON(fiber.Map{ "error_code": mwErr.Code, "message": mwErr.Message, }) }, }) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/", nil)) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read custom error response body: %v", err) } if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("expected custom error status 503, got %d", resp.StatusCode) } if !strings.Contains(string(body), `"error_code":"waf_not_initialized"`) { t.Fatalf("expected custom error code in body, got %q", string(body)) } } func TestEngineReportIncludesLifecycleSnapshot(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } report := engine.Report() if !report.Engine.Initialized { t.Fatal("expected report to include initialized engine state") } if report.Engine.InitSuccessTotal != 1 { t.Fatalf("expected init success count to be 1, got %d", report.Engine.InitSuccessTotal) } if len(report.Engine.ConfigFiles) != 1 { t.Fatalf("expected one config file in report, got %+v", report.Engine.ConfigFiles) } } func TestMetricsSnapshotHandlesNilCollectorSnapshot(t *testing.T) { engine := newEngine(nilSnapshotCollector{}) snapshot := engine.MetricsSnapshot() if snapshot.TotalRequests != 0 || snapshot.BlockedRequests != 0 || snapshot.AvgLatencyMs != 0 || snapshot.BlockRate != 0 { t.Fatalf("expected zero-value metrics snapshot, got %+v", snapshot) } if snapshot.Timestamp.IsZero() { t.Fatal("expected metrics snapshot timestamp to be populated") } } func TestNewEngineFallsBackToDefaultCollectorForTypedNilMetricsCollector(t *testing.T) { var collector MetricsCollector = (*nilPtrSnapshotCollector)(nil) engine := newEngine(collector) if engine.Metrics() == nil { t.Fatal("expected typed-nil metrics collector to fall back to the default collector") } app := newInstanceApp(engine, MiddlewareConfig{}) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/", nil)) defer resp.Body.Close() if resp.StatusCode != http.StatusInternalServerError { t.Fatalf("expected status 500 with uninitialized WAF, got %d", resp.StatusCode) } snapshot := engine.MetricsSnapshot() if snapshot.TotalRequests != 1 { t.Fatalf("expected fallback collector to record one request, got %+v", snapshot) } } func TestEngineInitFailureKeepsLastWorkingWAF(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } app := newInstanceApp(engine, MiddlewareConfig{}) allowedBefore := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?name=safe", nil)) defer allowedBefore.Body.Close() if allowedBefore.StatusCode != http.StatusOK { t.Fatalf("expected status 200 before failed reinit, got %d", allowedBefore.StatusCode) } err = engine.Init(Config{DirectivesFile: []string{filepath.Join(t.TempDir(), "missing.conf")}}) if err == nil { t.Fatal("expected reinitialization with missing config to fail") } allowedAfter := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?name=safe", nil)) defer allowedAfter.Body.Close() body, err := io.ReadAll(allowedAfter.Body) if err != nil { t.Fatalf("failed to read response body after failed reinit: %v", err) } if allowedAfter.StatusCode != http.StatusOK { t.Fatalf("expected last working WAF to continue serving after failed reinit, got %d with body %q", allowedAfter.StatusCode, string(body)) } snapshot := engine.Snapshot() if snapshot.LastInitError == "" { t.Fatal("expected engine snapshot to retain the last initialization error for observability") } if len(snapshot.ConfigFiles) != 1 || !strings.HasSuffix(snapshot.ConfigFiles[0], "test.conf") { t.Fatalf("expected active config to remain unchanged, got %+v", snapshot.ConfigFiles) } if len(snapshot.LastAttemptConfigFiles) != 1 || !strings.HasSuffix(snapshot.LastAttemptConfigFiles[0], "missing.conf") { t.Fatalf("expected last attempted config to be reported, got %+v", snapshot.LastAttemptConfigFiles) } } func TestMiddlewareFailsClosedWhenWAFPanicOccurs(t *testing.T) { engine := newEngine(NewDefaultMetricsCollector()) engine.waf = fakePanicWAF{} app := newInstanceApp(engine, MiddlewareConfig{}) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?name=safe", nil)) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read panic recovery response body: %v", err) } if resp.StatusCode != http.StatusInternalServerError { t.Fatalf("expected status 500 when WAF panics, got %d", resp.StatusCode) } if !strings.Contains(string(body), "WAF internal error") { t.Fatalf("expected WAF internal error response, got %q", string(body)) } metrics := engine.MetricsSnapshot() if metrics.TotalRequests != 1 || metrics.BlockedRequests != 0 { t.Fatalf("unexpected metrics after panic recovery: %+v", metrics) } } func TestEngineSnapshotTracksLifecycleCounters(t *testing.T) { engine, err := newTestEngine(t) if err != nil { t.Fatalf("failed to create engine: %v", err) } if err := engine.Reload(); err != nil { t.Fatalf("expected reload to succeed, got %v", err) } snapshot := engine.Snapshot() if snapshot.ReloadSuccessTotal != 1 { t.Fatalf("expected ReloadSuccessTotal=1, got %#v", snapshot.ReloadSuccessTotal) } if snapshot.InitSuccessTotal != 1 { t.Fatalf("expected InitSuccessTotal=1, got %#v", snapshot.InitSuccessTotal) } if snapshot.ReloadCount != 1 { t.Fatalf("expected ReloadCount=1, got %#v", snapshot.ReloadCount) } } func TestEngineInitReplacesMetricsCollectorWhenProvided(t *testing.T) { initialCollector := &countingCollector{} engine := newEngine(initialCollector) path := writeRuleFile(t, t.TempDir(), "collector.conf", testRules) replacementCollector := &countingCollector{} if err := engine.Init(Config{ DirectivesFile: []string{path}, RequestBodyAccess: true, MetricsCollector: replacementCollector, }); err != nil { t.Fatalf("expected init to succeed, got %v", err) } app := newInstanceApp(engine, MiddlewareConfig{}) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?name=safe", nil)) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status 200 after init, got %d", resp.StatusCode) } if initialCollector.requests != 0 { t.Fatalf("expected initial collector to stop receiving updates, got %d requests", initialCollector.requests) } if replacementCollector.requests != 1 { t.Fatalf("expected replacement collector to record one request, got %d", replacementCollector.requests) } } func TestEngineInitResetsMetricsCollectorToDefaultWhenOmitted(t *testing.T) { initialCollector := &countingCollector{} engine, err := NewEngine(Config{ DirectivesFile: []string{writeRuleFile(t, t.TempDir(), "collector.conf", testRules)}, }.WithMetricsCollector(initialCollector)) if err != nil { t.Fatalf("failed to create engine with custom collector: %v", err) } reloadPath := writeRuleFile(t, t.TempDir(), "collector-reload.conf", testRules) if err := engine.Init(Config{ DirectivesFile: []string{reloadPath}, }); err != nil { t.Fatalf("expected reinit without collector to succeed, got %v", err) } app := newInstanceApp(engine, MiddlewareConfig{}) resp := performRequest(t, app, httptest.NewRequest(http.MethodGet, "/?name=safe", nil)) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected status 200 after collector reset, got %d", resp.StatusCode) } if initialCollector.requests != 0 { t.Fatalf("expected original custom collector to stop receiving updates, got %d requests", initialCollector.requests) } snapshot := engine.MetricsSnapshot() if snapshot.TotalRequests != 1 { t.Fatalf("expected default collector to record one request after reset, got %+v", snapshot) } } func TestReloadWithoutDirectivesSucceeds(t *testing.T) { engine, err := NewEngine(Config{ RequestBodyAccess: true, }) if err != nil { t.Fatalf("failed to initialize engine without directives: %v", err) } if err := engine.Reload(); err != nil { t.Fatalf("expected reload without directives to succeed, got %v", err) } snapshot := engine.Snapshot() if snapshot.ReloadSuccessTotal != 1 { t.Fatalf("expected ReloadSuccessTotal=1, got %#v", snapshot.ReloadSuccessTotal) } if snapshot.InitSuccessTotal != 1 { t.Fatalf("expected InitSuccessTotal=1, got %#v", snapshot.InitSuccessTotal) } if snapshot.ReloadCount != 1 { t.Fatalf("expected ReloadCount=1, got %#v", snapshot.ReloadCount) } } func TestNewEngineWildcardDirectivesRequireMatch(t *testing.T) { _, err := NewEngine(Config{ DirectivesFile: []string{filepath.Join(t.TempDir(), "*.conf")}, }) if err == nil { t.Fatal("expected wildcard directives with no matches to fail") } if !strings.Contains(err.Error(), "matched no files") { t.Fatalf("expected wildcard match error, got %v", err) } } func TestNewEngineQuestionMarkGlobDirectivesMatch(t *testing.T) { rootDir := t.TempDir() writeRuleFile(t, rootDir, "rule-1.conf", testRules) engine, err := NewEngine(Config{ DirectivesFile: []string{filepath.Join(rootDir, "rule-?.conf")}, RequestBodyAccess: true, }) if err != nil { t.Fatalf("expected question mark glob to match directives file, got %v", err) } if engine == nil || engine.waf == nil { t.Fatal("expected engine to initialize from question mark glob") } } func TestNewEngineCharacterClassGlobDirectivesMatch(t *testing.T) { rootDir := t.TempDir() writeRuleFile(t, rootDir, "rule-1.conf", testRules) engine, err := NewEngine(Config{ DirectivesFile: []string{filepath.Join(rootDir, "rule-[12].conf")}, RequestBodyAccess: true, }) if err != nil { t.Fatalf("expected character class glob to match directives file, got %v", err) } if engine == nil || engine.waf == nil { t.Fatal("expected engine to initialize from character class glob") } } func TestNewEngineWildcardDirectivesWithRootFSRequireMatch(t *testing.T) { rootDir := t.TempDir() _, err := NewEngine(Config{ DirectivesFile: []string{"*.conf"}, RootFS: os.DirFS(rootDir), }) if err == nil { t.Fatal("expected RootFS wildcard directives with no matches to fail") } if !strings.Contains(err.Error(), "matched no files") { t.Fatalf("expected wildcard match error, got %v", err) } } func TestDefaultMetricsCollectorRecordLatencyUsesOnlineAverage(t *testing.T) { collector := NewDefaultMetricsCollector().(*defaultMetricsCollector) collector.RecordLatency(time.Millisecond) collector.RecordLatency(3 * time.Millisecond) collector.RecordLatency(-time.Millisecond) snapshot := collector.GetMetrics() if snapshot == nil { t.Fatal("expected metrics snapshot") } if collector.latencyCount != 2 { t.Fatalf("expected negative latency sample to be ignored, got %d", collector.latencyCount) } if snapshot.AvgLatencyMs != 2 { t.Fatalf("expected average latency to be 2ms, got %v", snapshot.AvgLatencyMs) } } func newInstanceApp(engine *Engine, cfg MiddlewareConfig) *fiber.App { app := fiber.New() app.Use(engine.Middleware(cfg)) app.All("/", func(c fiber.Ctx) error { return c.SendString("ok") }) return app } func newTestEngine(t *testing.T) (*Engine, error) { t.Helper() return newTestEngineWithRules(t, testRules) } func newTestEngineWithRules(t *testing.T, rules string) (*Engine, error) { t.Helper() path := writeRuleFile(t, t.TempDir(), "test.conf", rules) return NewEngine(Config{ LogLevel: fiberlog.LevelInfo, DirectivesFile: []string{path}, RequestBodyAccess: true, }) } func writeRuleFile(t *testing.T, dir, name, contents string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { t.Fatalf("failed to write directives file: %v", err) } return path } func performRequest(t *testing.T, app *fiber.App, req *http.Request) *http.Response { t.Helper() resp, err := app.Test(req) if err != nil { t.Fatalf("request failed: %v", err) } return resp } type nilSnapshotCollector struct{} func (nilSnapshotCollector) RecordRequest() {} func (nilSnapshotCollector) RecordBlock() {} func (nilSnapshotCollector) RecordLatency(time.Duration) {} func (nilSnapshotCollector) GetMetrics() *MetricsSnapshot { return nil } func (nilSnapshotCollector) Reset() {} type nilPtrSnapshotCollector struct{} func (*nilPtrSnapshotCollector) RecordRequest() {} func (*nilPtrSnapshotCollector) RecordBlock() {} func (*nilPtrSnapshotCollector) RecordLatency(time.Duration) {} func (*nilPtrSnapshotCollector) GetMetrics() *MetricsSnapshot { return nil } func (*nilPtrSnapshotCollector) Reset() {} type countingCollector struct { requests uint64 blocks uint64 } func (c *countingCollector) RecordRequest() { c.requests++ } func (c *countingCollector) RecordBlock() { c.blocks++ } func (c *countingCollector) RecordLatency(time.Duration) {} func (c *countingCollector) GetMetrics() *MetricsSnapshot { return &MetricsSnapshot{ TotalRequests: c.requests, BlockedRequests: c.blocks, Timestamp: time.Now(), } } func (c *countingCollector) Reset() { c.requests = 0 c.blocks = 0 } type fakePanicWAF struct{} func (fakePanicWAF) NewTransaction() types.Transaction { return fakePanicTransaction{} } func (fakePanicWAF) NewTransactionWithID(string) types.Transaction { return fakePanicTransaction{} } type fakePanicTransaction struct{} func (fakePanicTransaction) ProcessConnection(string, int, string, int) {} func (fakePanicTransaction) ProcessURI(string, string, string) {} func (fakePanicTransaction) SetServerName(string) {} func (fakePanicTransaction) AddRequestHeader(string, string) {} func (fakePanicTransaction) ProcessRequestHeaders() *types.Interruption { panic("boom") } func (fakePanicTransaction) RequestBodyReader() (io.Reader, error) { return bytes.NewReader(nil), nil } func (fakePanicTransaction) AddGetRequestArgument(string, string) {} func (fakePanicTransaction) AddPostRequestArgument(string, string) {} func (fakePanicTransaction) AddPathRequestArgument(string, string) {} func (fakePanicTransaction) AddResponseArgument(string, string) {} func (fakePanicTransaction) ProcessRequestBody() (*types.Interruption, error) { return nil, nil } func (fakePanicTransaction) WriteRequestBody([]byte) (*types.Interruption, int, error) { return nil, 0, nil } func (fakePanicTransaction) ReadRequestBodyFrom(io.Reader) (*types.Interruption, int, error) { return nil, 0, nil } func (fakePanicTransaction) AddResponseHeader(string, string) {} func (fakePanicTransaction) ProcessResponseHeaders(int, string) *types.Interruption { return nil } func (fakePanicTransaction) ResponseBodyReader() (io.Reader, error) { return bytes.NewReader(nil), nil } func (fakePanicTransaction) ProcessResponseBody() (*types.Interruption, error) { return nil, nil } func (fakePanicTransaction) WriteResponseBody([]byte) (*types.Interruption, int, error) { return nil, 0, nil } func (fakePanicTransaction) ReadResponseBodyFrom(io.Reader) (*types.Interruption, int, error) { return nil, 0, nil } func (fakePanicTransaction) ProcessLogging() {} func (fakePanicTransaction) IsRuleEngineOff() bool { return false } func (fakePanicTransaction) IsRequestBodyAccessible() bool { return false } func (fakePanicTransaction) IsResponseBodyAccessible() bool { return false } func (fakePanicTransaction) IsResponseBodyProcessable() bool { return false } func (fakePanicTransaction) IsInterrupted() bool { return false } func (fakePanicTransaction) Interruption() *types.Interruption { return nil } func (fakePanicTransaction) MatchedRules() []types.MatchedRule { return nil } func (fakePanicTransaction) DebugLogger() debuglog.Logger { return nil } func (fakePanicTransaction) ID() string { return "panic-tx" } func (fakePanicTransaction) Close() error { return nil } ================================================ FILE: v3/coraza/go.mod ================================================ module github.com/gofiber/contrib/v3/coraza go 1.26.2 require ( github.com/corazawaf/coraza/v3 v3.7.0 github.com/gofiber/fiber/v3 v3.1.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/corazawaf/libinjection-go v0.3.2 // indirect github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/kaptinlin/go-i18n v0.4.0 // indirect github.com/kaptinlin/jsonpointer v0.4.18 // indirect github.com/kaptinlin/jsonschema v0.7.7 // indirect github.com/kaptinlin/messageformat-go v0.4.20 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/magefile/mage v1.17.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect google.golang.org/protobuf v1.36.11 // indirect rsc.io/binaryregexp v0.2.0 // indirect ) ================================================ FILE: v3/coraza/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc= github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU= github.com/corazawaf/coraza/v3 v3.7.0 h1:LIQqu1r+l6e/U/gyiZeykWaNNBY1TzRLz+aaI+QYEEM= github.com/corazawaf/coraza/v3 v3.7.0/go.mod h1:dOSt5evqC7EstouEv6ghhui01+oVUwp9X1vybWwqTlo= github.com/corazawaf/libinjection-go v0.3.2 h1:9rrKt0lpg4WvUXt+lwS06GywfqRXXsa/7JcOw5cQLwI= github.com/corazawaf/libinjection-go v0.3.2/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jcchavezs/mergefs v0.1.1 h1:D45R17m6dHnSVZefnhynoeZvcK2Uw0oTrRfoUOQ0S5Y= github.com/jcchavezs/mergefs v0.1.1/go.mod h1:eRLTrsA+vFwQZ48hj8p8gki/5v9C2bFtHH5Mnn4bcGk= github.com/kaptinlin/go-i18n v0.4.0 h1:i7L3U2yurg+xhokITtJ0k+mjHnXqkoyz8ju5Wb7W8Oc= github.com/kaptinlin/go-i18n v0.4.0/go.mod h1:njA6x0+4MWGcLWT0KLrwekhRPmze1Hnstf2+VJFzwpM= github.com/kaptinlin/jsonpointer v0.4.18 h1:EDUXT4WKpOKguU7oaFv6VaNatN7uHFe6dEYHX0+OFxs= github.com/kaptinlin/jsonpointer v0.4.18/go.mod h1:ndmfvrqrEDSbV3F7yGaOuDvr29WrxYU1aqkvef9L2do= github.com/kaptinlin/jsonschema v0.7.7 h1:41BlQJ9dskH0oE5DSzBUrl/w4JQYIr6N6L0B5GNyDoM= github.com/kaptinlin/jsonschema v0.7.7/go.mod h1:rKjWfyySHSxAD7Li2ctYkPlOu960igoKBvZ2ADRtd5Q= github.com/kaptinlin/messageformat-go v0.4.20 h1:a0ufTd5liiUubIGeGxpSTnNS8ZSrN4DV01/wGFmfzMs= github.com/kaptinlin/messageformat-go v0.4.20/go.mod h1:FqdEPfQLkqVBX7OBRMPgYwUPvKYJohFD9Ok1BMzCfIo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/magefile/mage v1.17.1 h1:F1d2lnLSlbQDM0Plq6Ac4NtaHxkxTK8t5nrMY9SkoNA= github.com/magefile/mage v1.17.1/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI= github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw= github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= ================================================ FILE: v3/coraza/metrics.go ================================================ // Package coraza includes lightweight metrics and lifecycle snapshots for Engine instances. package coraza import ( "sync" "sync/atomic" "time" ) // MetricsCollector records lightweight request metrics for a Coraza Engine. type MetricsCollector interface { RecordRequest() RecordBlock() RecordLatency(duration time.Duration) GetMetrics() *MetricsSnapshot Reset() } // MetricsSnapshot represents the current request metrics for an Engine. type MetricsSnapshot struct { // TotalRequests is the number of requests observed by the middleware. TotalRequests uint64 `json:"total_requests"` // BlockedRequests is the number of requests interrupted by the WAF. BlockedRequests uint64 `json:"blocked_requests"` // AvgLatencyMs is the average middleware latency in milliseconds. AvgLatencyMs float64 `json:"avg_latency_ms"` // BlockRate is the ratio of blocked requests to total requests. BlockRate float64 `json:"block_rate"` // Timestamp is when the snapshot was generated. Timestamp time.Time `json:"timestamp"` } // EngineSnapshot represents lifecycle and configuration state for an Engine. type EngineSnapshot struct { // Initialized reports whether the Engine currently holds a usable WAF instance. Initialized bool `json:"initialized"` // SupportsOptions reports whether the current WAF supports Coraza experimental options. SupportsOptions bool `json:"supports_options"` // ConfigFiles lists the directive files for the active configuration. ConfigFiles []string `json:"config_files"` // LastAttemptConfigFiles lists the directive files from the most recent init attempt. LastAttemptConfigFiles []string `json:"last_attempt_config_files"` // LastInitError contains the most recent initialization error, if any. LastInitError string `json:"last_init_error,omitempty"` // LastLoadedAt is the timestamp of the most recent successful initialization or reload. LastLoadedAt time.Time `json:"last_loaded_at"` // InitSuccessTotal is the number of successful init calls. InitSuccessTotal uint64 `json:"init_success_total"` // InitFailureTotal is the number of failed init calls. InitFailureTotal uint64 `json:"init_failure_total"` // ReloadSuccessTotal is the number of successful reload calls. ReloadSuccessTotal uint64 `json:"reload_success_total"` // ReloadFailureTotal is the number of failed reload calls. ReloadFailureTotal uint64 `json:"reload_failure_total"` // ReloadCount is the total number of successful reload transitions. ReloadCount uint64 `json:"reload_count"` } // MetricsReport combines request metrics with Engine lifecycle information. type MetricsReport struct { // Requests is the request metrics snapshot. Requests MetricsSnapshot `json:"requests"` // Engine is the Engine lifecycle snapshot. Engine EngineSnapshot `json:"engine"` } type defaultMetricsCollector struct { totalRequests atomic.Uint64 blockedRequests atomic.Uint64 latencyMutex sync.RWMutex avgLatencyNs float64 latencyCount uint64 } // NewDefaultMetricsCollector creates the built-in in-memory metrics collector. func NewDefaultMetricsCollector() MetricsCollector { return &defaultMetricsCollector{} } func (m *defaultMetricsCollector) RecordRequest() { m.totalRequests.Add(1) } func (m *defaultMetricsCollector) RecordBlock() { m.blockedRequests.Add(1) } func (m *defaultMetricsCollector) RecordLatency(duration time.Duration) { if duration < 0 { return } m.latencyMutex.Lock() defer m.latencyMutex.Unlock() m.latencyCount++ count := float64(m.latencyCount) m.avgLatencyNs += (float64(duration.Nanoseconds()) - m.avgLatencyNs) / count } func (m *defaultMetricsCollector) GetMetrics() *MetricsSnapshot { totalReqs := m.totalRequests.Load() blockedReqs := m.blockedRequests.Load() m.latencyMutex.RLock() var avgLatencyMs float64 if m.latencyCount > 0 { avgLatencyMs = m.avgLatencyNs / 1e6 } m.latencyMutex.RUnlock() var blockRate float64 if totalReqs > 0 { blockRate = float64(blockedReqs) / float64(totalReqs) } return &MetricsSnapshot{ TotalRequests: totalReqs, BlockedRequests: blockedReqs, AvgLatencyMs: avgLatencyMs, BlockRate: blockRate, Timestamp: time.Now(), } } func (m *defaultMetricsCollector) Reset() { m.totalRequests.Store(0) m.blockedRequests.Store(0) m.latencyMutex.Lock() defer m.latencyMutex.Unlock() m.avgLatencyNs = 0 m.latencyCount = 0 } // MetricsSnapshot returns a copy of the Engine's current request metrics. func (e *Engine) MetricsSnapshot() MetricsSnapshot { collector := e.Metrics() if collector == nil { return MetricsSnapshot{Timestamp: time.Now()} } snapshot := collector.GetMetrics() if snapshot == nil { return MetricsSnapshot{Timestamp: time.Now()} } return *snapshot } // Snapshot returns lifecycle and configuration state for the Engine. func (e *Engine) Snapshot() EngineSnapshot { return e.observabilitySnapshot() } // Report returns both the request metrics and lifecycle snapshot for the Engine. func (e *Engine) Report() MetricsReport { return MetricsReport{ Requests: e.MetricsSnapshot(), Engine: e.Snapshot(), } } ================================================ FILE: v3/fgprof/README.md ================================================ --- id: fgprof --- # Fgprof ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*fgprof*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20Fgprof/badge.svg) [fgprof](https://github.com/felixge/fgprof) support for Fiber. **Compatible with Fiber v3.** ## 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. ## Install Using fgprof to profiling your Fiber app. ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/fgprof ``` ## Config | Property | Type | Description | Default | |----------|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|---------| | Next | `func(c fiber.Ctx) bool` | A function to skip this middleware when returned `true`. | `nil` | | 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" | `""` | ## Example ```go package main import ( "log" "github.com/gofiber/contrib/v3/fgprof" "github.com/gofiber/fiber/v3" ) func main() { app := fiber.New() app.Use(fgprof.New()) app.Get("/", func(c fiber.Ctx) error { return c.SendString("OK") }) log.Fatal(app.Listen(":3000")) } ``` ```bash go tool pprof -http=:8080 http://localhost:3000/debug/fgprof ``` ================================================ FILE: v3/fgprof/config.go ================================================ package fgprof import "github.com/gofiber/fiber/v3" type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool // Prefix is the path where the fprof endpoints will be mounted. // Default Path is "/debug/fgprof" // // Optional. Default: "" Prefix string } // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, } func configDefault(config ...Config) Config { // Return default config if nothing provided if len(config) < 1 { return ConfigDefault } // Override default config cfg := config[0] // Set default values if cfg.Next == nil { cfg.Next = ConfigDefault.Next } return cfg } ================================================ FILE: v3/fgprof/fgprof.go ================================================ package fgprof import ( "github.com/felixge/fgprof" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" ) func New(conf ...Config) fiber.Handler { // Set default config cfg := configDefault(conf...) fgProfPath := cfg.Prefix + "/debug/fgprof" var fgprofHandler = adaptor.HTTPHandler(fgprof.Handler()) // Return new handler return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } if c.Path() == fgProfPath { return fgprofHandler(c) } return c.Next() } } ================================================ FILE: v3/fgprof/fgprof_test.go ================================================ package fgprof import ( "io" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" ) // go test -run Test_Non_Fgprof_Path func Test_Non_Fgprof_Path(t *testing.T) { app := fiber.New() app.Use(New()) app.Get("/", func(c fiber.Ctx) error { return c.SendString("escaped") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) body, err := io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, "escaped", string(body)) } // go test -run Test_Non_Fgprof_Path_WithPrefix func Test_Non_Fgprof_Path_WithPrefix(t *testing.T) { app := fiber.New() app.Use(New(Config{ Prefix: "/prefix", })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("escaped") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) body, err := io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, "escaped", string(body)) } // go test -run Test_Fgprof_Path func Test_Fgprof_Path(t *testing.T) { app := fiber.New() app.Use(New()) // Default fgprof interval is 30 seconds resp, err := app.Test(httptest.NewRequest("GET", "/debug/fgprof?seconds=1", nil), fiber.TestConfig{Timeout: 3000}) if err != nil && strings.Contains(err.Error(), "empty response") { t.Skip("fiber test helper returns empty response for streaming endpoints") } assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } // go test -run Test_Fgprof_Path_WithPrefix func Test_Fgprof_Path_WithPrefix(t *testing.T) { app := fiber.New() app.Use(New(Config{ Prefix: "/test", })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("escaped") }) // Non fgprof prefix path resp, err := app.Test(httptest.NewRequest("GET", "/prefix/debug/fgprof?seconds=1", nil), fiber.TestConfig{Timeout: 3000}) if err != nil && strings.Contains(err.Error(), "empty response") { t.Skip("fiber test helper returns empty response for streaming endpoints") } assert.Equal(t, nil, err) assert.Equal(t, 404, resp.StatusCode) // Fgprof prefix path resp, err = app.Test(httptest.NewRequest("GET", "/test/debug/fgprof?seconds=1", nil), fiber.TestConfig{Timeout: 3000}) if err != nil && strings.Contains(err.Error(), "empty response") { t.Skip("fiber test helper returns empty response for streaming endpoints") } assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } // go test -run Test_Fgprof_Next func Test_Fgprof_Next(t *testing.T) { app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { return true }, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/pprof/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 404, resp.StatusCode) } // go test -run Test_Fgprof_Next_WithPrefix func Test_Fgprof_Next_WithPrefix(t *testing.T) { app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { return true }, Prefix: "/federated-fiber", })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 404, resp.StatusCode) } ================================================ FILE: v3/fgprof/go.mod ================================================ module github.com/gofiber/contrib/v3/fgprof go 1.25.0 require ( github.com/felixge/fgprof v0.9.5 github.com/gofiber/fiber/v3 v3.1.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/fgprof/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/hcaptcha/README.md ================================================ --- id: hcaptcha --- # HCaptcha ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*hcaptcha*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20hcaptcha/badge.svg) A simple [HCaptcha](https://hcaptcha.com) middleware to prevent bot attacks. :::note Requires Go **1.25** and above ::: **Compatible with Fiber v3.** ## 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. ## Install :::caution This middleware only supports Fiber **v3**. ::: ```shell go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/hcaptcha ``` ## Signature ```go hcaptcha.New(config hcaptcha.Config) fiber.Handler ``` ## Config | Property | Type | Description | Default | |:----------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------| | SecretKey | `string` | The secret key you obtained from the HCaptcha admin panel. This field must not be empty. | `""` | | 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` | | SiteVerifyURL | `string` | This property specifies the API resource used for token authentication. | `https://api.hcaptcha.com/siteverify` | | 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` | ## Example ```go package main import ( "errors" "log" "github.com/gofiber/contrib/v3/hcaptcha" "github.com/gofiber/fiber/v3" ) const ( TestSecretKey = "0x0000000000000000000000000000000000000000" TestSiteKey = "20000000-ffff-ffff-ffff-000000000002" ) func main() { app := fiber.New() captcha := hcaptcha.New(hcaptcha.Config{ // Must set the secret key. SecretKey: TestSecretKey, // Optional custom validation handling. ValidateFunc: func(success bool, c fiber.Ctx) error { if !success { if err := c.Status(fiber.StatusForbidden).JSON(fiber.Map{ "error": "HCaptcha validation failed", "details": "Please complete the captcha challenge and try again", }); err != nil { return err } return errors.New("custom validation failed") } return nil }, }) app.Get("/api/", func(c fiber.Ctx) error { return c.JSON(fiber.Map{ "hcaptcha_site_key": TestSiteKey, }) }) // Middleware order matters: place hcaptcha middleware before the final handler. app.Post("/api/submit", captcha, func(c fiber.Ctx) error { return c.SendString("You are not a robot") }) log.Fatal(app.Listen(":3000")) } ``` ================================================ FILE: v3/hcaptcha/config.go ================================================ package hcaptcha import ( "bytes" "encoding/json" "fmt" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" ) // DefaultSiteVerifyURL is the default URL for the HCaptcha API const DefaultSiteVerifyURL = "https://api.hcaptcha.com/siteverify" // Config defines the config for HCaptcha middleware. type Config struct { // SecretKey is the secret key you get from HCaptcha when you create a new application SecretKey string // ResponseKeyFunc should return the generated pass UUID from the ctx, which will be validated ResponseKeyFunc func(fiber.Ctx) (string, error) // SiteVerifyURL is the endpoint URL where the program should verify the given token // default value is: "https://api.hcaptcha.com/siteverify" SiteVerifyURL string // ValidateFunc allows custom validation handling based on the HCaptcha validation result. // If set, it is called with the API success status and the current context after siteverify. // For secure bot protection, reject requests when success is false. // Return nil to continue to the next handler, or return an error to stop the middleware chain. // If ValidateFunc is nil, default behavior is used and unsuccessful verification returns 403. ValidateFunc func(success bool, c fiber.Ctx) error } // DefaultResponseKeyFunc is the default function to get the HCaptcha token from the request body func DefaultResponseKeyFunc(c fiber.Ctx) (string, error) { data := struct { HCaptchaToken string `json:"hcaptcha_token"` }{} err := json.NewDecoder(bytes.NewReader(c.Body())).Decode(&data) if err != nil { return "", fmt.Errorf("failed to decode HCaptcha token: %w", err) } if utils.TrimSpace(data.HCaptchaToken) == "" { return "", fmt.Errorf("hcaptcha token is empty") } return data.HCaptchaToken, nil } ================================================ FILE: v3/hcaptcha/go.mod ================================================ module github.com/gofiber/contrib/v3/hcaptcha go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/stretchr/testify v1.11.1 github.com/valyala/fasthttp v1.70.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/hcaptcha/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/hcaptcha/hcaptcha.go ================================================ // Package hcaptcha is a simple middleware that checks for an HCaptcha UUID // and then validates it. It returns an error if the UUID is not valid (the request may have been sent by a robot). package hcaptcha import ( "bytes" "encoding/json" "errors" "fmt" "github.com/gofiber/fiber/v3" "github.com/valyala/fasthttp" "net/url" ) // HCaptcha is a middleware handler that checks for an HCaptcha UUID and then validates it. type HCaptcha struct { Config } // New creates a new HCaptcha middleware handler. func New(config Config) fiber.Handler { if config.SiteVerifyURL == "" { config.SiteVerifyURL = DefaultSiteVerifyURL } if config.ResponseKeyFunc == nil { config.ResponseKeyFunc = DefaultResponseKeyFunc } h := &HCaptcha{ config, } return h.Validate } // Validate checks for an HCaptcha UUID and then validates it. func (h *HCaptcha) Validate(c fiber.Ctx) error { token, err := h.ResponseKeyFunc(c) if err != nil { c.Status(fiber.StatusBadRequest) return fmt.Errorf("error retrieving HCaptcha token: %w", err) } req := fasthttp.AcquireRequest() defer fasthttp.ReleaseRequest(req) req.SetBody([]byte(url.Values{ "secret": {h.SecretKey}, "response": {token}, }.Encode())) req.Header.SetMethod("POST") req.Header.SetContentType("application/x-www-form-urlencoded; charset=UTF-8") req.Header.Set("Accept", "application/json") req.SetRequestURI(h.SiteVerifyURL) res := fasthttp.AcquireResponse() defer fasthttp.ReleaseResponse(res) // Send the request to the HCaptcha API if err = fasthttp.Do(req, res); err != nil { c.Status(fiber.StatusBadRequest) return fmt.Errorf("error sending request to HCaptcha API: %w", err) } o := struct { Success bool `json:"success"` }{} if err = json.NewDecoder(bytes.NewReader(res.Body())).Decode(&o); err != nil { c.Status(fiber.StatusInternalServerError) return fmt.Errorf("error decoding HCaptcha API response: %w", err) } // Execute custom validation if ValidateFunc is defined. // ValidateFunc receives the siteverify result and should return an error on validation failure. // If ValidateFunc is nil, default behavior rejects unsuccessful verification. var validationErr error if h.ValidateFunc != nil { validationErr = h.ValidateFunc(o.Success, c) } else if !o.Success { validationErr = errors.New("unable to check that you are not a robot") } if validationErr != nil { statusCode := c.Response().StatusCode() if statusCode == 0 || statusCode == fiber.StatusOK { c.Status(fiber.StatusForbidden) } if len(c.Response().Body()) == 0 { return c.SendString(validationErr.Error()) } return nil } return c.Next() } ================================================ FILE: v3/hcaptcha/hcaptcha_test.go ================================================ package hcaptcha import ( "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( TestSecretKey = "0x0000000000000000000000000000000000000000" TestResponseToken = "20000000-aaaa-bbbb-cccc-000000000002" ) func newSiteVerifyServer(t *testing.T, success bool) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, "application/json", r.Header.Get("Accept")) _, err := io.ReadAll(r.Body) require.NoError(t, err) require.NoError(t, r.Body.Close()) _, err = w.Write([]byte(`{"success":` + map[bool]string{true: "true", false: "false"}[success] + `}`)) require.NoError(t, err) })) } func TestHCaptchaDefaultValidation(t *testing.T) { t.Run("success", func(t *testing.T) { server := newSiteVerifyServer(t, true) defer server.Close() app := fiber.New() m := New(Config{ SecretKey: TestSecretKey, SiteVerifyURL: server.URL, ResponseKeyFunc: func(c fiber.Ctx) (string, error) { return TestResponseToken, nil }, }) app.Get("/hcaptcha", m, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodGet, "/hcaptcha", nil) res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, res.StatusCode) assert.Equal(t, "ok", string(body)) }) t.Run("failure", func(t *testing.T) { server := newSiteVerifyServer(t, false) defer server.Close() app := fiber.New() m := New(Config{ SecretKey: TestSecretKey, SiteVerifyURL: server.URL, ResponseKeyFunc: func(c fiber.Ctx) (string, error) { return TestResponseToken, nil }, }) app.Get("/hcaptcha", m, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodGet, "/hcaptcha", nil) res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, fiber.StatusForbidden, res.StatusCode) assert.Equal(t, "unable to check that you are not a robot", string(body)) }) } func TestHCaptchaValidateFunc(t *testing.T) { t.Run("called with success and allows request", func(t *testing.T) { server := newSiteVerifyServer(t, true) defer server.Close() app := fiber.New() called := false m := New(Config{ SecretKey: TestSecretKey, SiteVerifyURL: server.URL, ResponseKeyFunc: func(c fiber.Ctx) (string, error) { return TestResponseToken, nil }, ValidateFunc: func(success bool, c fiber.Ctx) error { called = true assert.True(t, success) return nil }, }) app.Get("/hcaptcha", m, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodGet, "/hcaptcha", nil) res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() assert.Equal(t, fiber.StatusOK, res.StatusCode) assert.True(t, called) }) t.Run("custom status is preserved on validation error", func(t *testing.T) { server := newSiteVerifyServer(t, false) defer server.Close() app := fiber.New() m := New(Config{ SecretKey: TestSecretKey, SiteVerifyURL: server.URL, ResponseKeyFunc: func(c fiber.Ctx) (string, error) { return TestResponseToken, nil }, ValidateFunc: func(success bool, c fiber.Ctx) error { assert.False(t, success) c.Status(fiber.StatusUnprocessableEntity) return errors.New("custom validation failed") }, }) app.Get("/hcaptcha", m, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodGet, "/hcaptcha", nil) res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, fiber.StatusUnprocessableEntity, res.StatusCode) assert.Equal(t, "custom validation failed", string(body)) }) t.Run("defaults to 403 and error body when validatefunc sets neither", func(t *testing.T) { server := newSiteVerifyServer(t, false) defer server.Close() app := fiber.New() m := New(Config{ SecretKey: TestSecretKey, SiteVerifyURL: server.URL, ResponseKeyFunc: func(c fiber.Ctx) (string, error) { return TestResponseToken, nil }, ValidateFunc: func(success bool, c fiber.Ctx) error { assert.False(t, success) return errors.New("custom validation failed") }, }) app.Get("/hcaptcha", m, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest(http.MethodGet, "/hcaptcha", nil) res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, fiber.StatusForbidden, res.StatusCode) assert.Equal(t, "custom validation failed", string(body)) }) } func TestDefaultResponseKeyFunc(t *testing.T) { app := fiber.New() app.Post("/", func(c fiber.Ctx) error { token, err := DefaultResponseKeyFunc(c) if err != nil { return c.Status(fiber.StatusBadRequest).SendString(err.Error()) } return c.SendString(token) }) t.Run("valid token", func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"hcaptcha_token":"abc"}`)) req.Header.Set("Content-Type", "application/json") res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, res.StatusCode) assert.Equal(t, "abc", string(body)) }) t.Run("empty token", func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"hcaptcha_token":" "}`)) req.Header.Set("Content-Type", "application/json") res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) require.NoError(t, err) defer res.Body.Close() body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, fiber.StatusBadRequest, res.StatusCode) assert.Equal(t, "hcaptcha token is empty", string(body)) }) } ================================================ FILE: v3/i18n/README.md ================================================ --- id: i18n --- # I18n ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*i18n*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20i18n/badge.svg) [go-i18n](https://github.com/nicksnyder/go-i18n) support for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/i18n ``` ## API | Name | Signature | Description | |----------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------------------| | New | `New(config ...*i18n.Config) *i18n.I18n` | Create a reusable, thread-safe localization container. | | (*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. | | (*I18n).MustLocalize | `MustLocalize(ctx fiber.Ctx, params interface{}) string` | Like `Localize` but panics on any error. | ## Types | Name | Description | |------------------|-----------------------------------------------------------------------------------------------------| | `Loader` | Interface for loading message files. Implement `LoadMessage(path string) ([]byte, error)`. | | `LoaderFunc` | Adapter to use a plain function as a `Loader`. | | `EmbedLoader` | `Loader` implementation backed by an `embed.FS`. Use with Go's `//go:embed` directive. | ## Config | Property | Type | Description | Default | |------------------|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------| | RootPath | `string` | The i18n template folder path. | `"./example/localize"` | | AcceptLanguages | `[]language.Tag` | A collection of languages that can be processed. | `[]language.Tag{language.Chinese, language.English}` | | FormatBundleFile | `string` | The type of the template file. | `"yaml"` | | DefaultLanguage | `language.Tag` | The default returned language type. | `language.English` | | 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)` | | UnmarshalFunc | `i18n.UnmarshalFunc` | The function used for decoding template files. | `yaml.Unmarshal` | | 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`. | ## Example ```go package main import ( "log" contribi18n "github.com/gofiber/contrib/v3/i18n" "github.com/gofiber/fiber/v3" goi18n "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) func main() { translator := contribi18n.New(&contribi18n.Config{ RootPath: "./example/localize", AcceptLanguages: []language.Tag{language.Chinese, language.English}, DefaultLanguage: language.Chinese, }) app := fiber.New() app.Get("/", func(c fiber.Ctx) error { localize, err := translator.Localize(c, "welcome") if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return c.SendString(localize) }) app.Get("/:name", func(ctx fiber.Ctx) error { return ctx.SendString(translator.MustLocalize(ctx, &goi18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": ctx.Params("name"), }, })) }) log.Fatal(app.Listen(":3000")) } ``` ## Migration from middleware usage The package now exposes a global, thread-safe container instead of middleware. To migrate existing code: 1. Remove any `app.Use(i18n.New(...))` calls—the translator no longer registers middleware. 2. Instantiate a shared translator during application startup with `translator := i18n.New(...)`. 3. Replace package-level calls such as `i18n.Localize`/`i18n.MustLocalize` with the respective methods on your translator (`translator.Localize`, `translator.MustLocalize`). 4. Drop any manual interaction with `ctx.Locals("i18n")`; all state is managed inside the translator instance. The translator instance is safe for concurrent use across handlers and reduces per-request allocations by reusing the same bundle and localizer map. ================================================ FILE: v3/i18n/config.go ================================================ package i18n import ( "os" "sync" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" "gopkg.in/yaml.v2" ) type Config struct { // RootPath is i18n template folder path // // Default: ./example/localize RootPath string // AcceptLanguages is a collection of languages that can be processed // // Optional. Default: []language.Tag{language.Chinese, language.English} AcceptLanguages []language.Tag // FormatBundleFile is type of template file. // // Optional. Default: "yaml" FormatBundleFile string // DefaultLanguage is the default returned language type // // Optional. Default: language.English DefaultLanguage language.Tag // Loader implements the Loader interface, which defines how to read the file. // We provide both os.ReadFile and embed.FS.ReadFile // Optional. Default: LoaderFunc(os.ReadFile) Loader Loader // UnmarshalFunc for decoding template files // // Optional. Default: yaml.Unmarshal UnmarshalFunc i18n.UnmarshalFunc // LangHandler is used to get the kind of language handled by fiber.Ctx and defaultLang // // Optional. Default: The language type is retrieved from the request header: `Accept-Language` or query param : `lang` LangHandler func(ctx fiber.Ctx, defaultLang string) string bundle *i18n.Bundle localizerMap *sync.Map } type Loader interface { LoadMessage(path string) ([]byte, error) } type LoaderFunc func(path string) ([]byte, error) func (f LoaderFunc) LoadMessage(path string) ([]byte, error) { return f(path) } var ConfigDefault = &Config{ RootPath: "./example/localize", DefaultLanguage: language.English, AcceptLanguages: []language.Tag{language.Chinese, language.English}, FormatBundleFile: "yaml", UnmarshalFunc: yaml.Unmarshal, Loader: LoaderFunc(os.ReadFile), LangHandler: defaultLangHandler, } func defaultLangHandler(c fiber.Ctx, defaultLang string) string { if c == nil || c.Request() == nil { return defaultLang } if lang := c.Query("lang"); lang != "" { return utils.CopyString(lang) } if lang := c.Get("Accept-Language"); lang != "" { return utils.CopyString(lang) } return defaultLang } func configDefault(config ...*Config) *Config { var cfg *Config switch { case len(config) == 0 || config[0] == nil: copyCfg := *ConfigDefault // ensure mutable fields are not shared with defaults if copyCfg.AcceptLanguages != nil { copyCfg.AcceptLanguages = append([]language.Tag(nil), copyCfg.AcceptLanguages...) } cfg = ©Cfg default: copyCfg := *config[0] if config[0].AcceptLanguages != nil { copyCfg.AcceptLanguages = append([]language.Tag(nil), config[0].AcceptLanguages...) } cfg = ©Cfg } if cfg.RootPath == "" { cfg.RootPath = ConfigDefault.RootPath } if cfg.DefaultLanguage == language.Und { cfg.DefaultLanguage = ConfigDefault.DefaultLanguage } if cfg.FormatBundleFile == "" { cfg.FormatBundleFile = ConfigDefault.FormatBundleFile } if cfg.UnmarshalFunc == nil { cfg.UnmarshalFunc = ConfigDefault.UnmarshalFunc } if cfg.AcceptLanguages == nil { cfg.AcceptLanguages = append([]language.Tag(nil), ConfigDefault.AcceptLanguages...) } if cfg.Loader == nil { cfg.Loader = ConfigDefault.Loader } if cfg.LangHandler == nil { cfg.LangHandler = ConfigDefault.LangHandler } return cfg } ================================================ FILE: v3/i18n/embed.go ================================================ //go:build go1.16 package i18n import "embed" type EmbedLoader struct { FS embed.FS } func (e *EmbedLoader) LoadMessage(path string) ([]byte, error) { return e.FS.ReadFile(path) } ================================================ FILE: v3/i18n/embed_test.go ================================================ package i18n import ( "context" "embed" "encoding/json" "io" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) //go:embed example/localizeJSON/* var fs embed.FS func newEmbedServer() *fiber.App { translator := New(&Config{ Loader: &EmbedLoader{fs}, UnmarshalFunc: json.Unmarshal, RootPath: "./example/localizeJSON/", FormatBundleFile: "json", }) app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendString(translator.MustLocalize(ctx, "welcome")) }) app.Get("/:name", func(ctx fiber.Ctx) error { return ctx.SendString(translator.MustLocalize(ctx, &i18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": ctx.Params("name"), }, })) }) return app } var embedApp = newEmbedServer() func request(lang language.Tag, name string) (*http.Response, error) { path := "/" + name req, _ := http.NewRequestWithContext(context.Background(), "GET", path, nil) req.Host = "localhost" req.Header.Add("Accept-Language", lang.String()) req.Method = "GET" req.RequestURI = path resp, err := embedApp.Test(req) return resp, err } func TestEmbedLoader_LoadMessage(t *testing.T) { t.Parallel() type args struct { lang language.Tag name string } tests := []struct { name string args args want string }{ { name: "hello world", args: args{ name: "", lang: language.English, }, want: "hello", }, { name: "hello alex", args: args{ name: "", lang: language.Chinese, }, want: "你好", }, { name: "hello alex", args: args{ name: "alex", lang: language.English, }, want: "hello alex", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := request(tt.args.lang, tt.args.name) assert.Equal(t, err, nil) body, err := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, err, nil) assert.Equal(t, tt.want, string(body)) }) } } ================================================ FILE: v3/i18n/example/localize/en.yaml ================================================ welcome: hello welcomeWithName: hello {{ .name }} ================================================ FILE: v3/i18n/example/localize/zh.yaml ================================================ welcome: 你好 welcomeWithName: 你好 {{ .name }} ================================================ FILE: v3/i18n/example/localizeJSON/en.json ================================================ { "welcome": "hello", "welcomeWithName": "hello {{ .name }}" } ================================================ FILE: v3/i18n/example/localizeJSON/zh.json ================================================ { "welcome": "你好", "welcomeWithName": "你好 {{ .name }}" } ================================================ FILE: v3/i18n/example/main.go ================================================ package main import ( "log" contribi18n "github.com/gofiber/contrib/v3/i18n" "github.com/gofiber/fiber/v3" goi18n "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) func main() { translator := contribi18n.New(&contribi18n.Config{ RootPath: "./localize", AcceptLanguages: []language.Tag{language.Chinese, language.English}, DefaultLanguage: language.Chinese, }) app := fiber.New() app.Get("/", func(c fiber.Ctx) error { localize, err := translator.Localize(c, "welcome") if err != nil { return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return c.SendString(localize) }) app.Get("/:name", func(ctx fiber.Ctx) error { return ctx.SendString(translator.MustLocalize(ctx, &goi18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": ctx.Params("name"), }, })) }) log.Fatal(app.Listen(":3000")) } ================================================ FILE: v3/i18n/go.mod ================================================ module github.com/gofiber/contrib/v3/i18n go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/stretchr/testify v1.11.1 golang.org/x/text v0.36.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/i18n/go.sum ================================================ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/i18n/i18n.go ================================================ package i18n import ( "fmt" "path" "sync" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/log" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) // I18n exposes thread-safe localization helpers backed by a shared bundle // and localizer map. Use New to construct an instance during application start // and reuse it across handlers. type I18n struct { cfg *Config } // New prepares a thread-safe i18n container instance. func New(config ...*Config) *I18n { cfg := prepareConfig(config...) return &I18n{cfg: cfg} } func prepareConfig(config ...*Config) *Config { source := configDefault(config...) cfg := *source if source.AcceptLanguages != nil { cfg.AcceptLanguages = append([]language.Tag(nil), source.AcceptLanguages...) } bundle := i18n.NewBundle(cfg.DefaultLanguage) bundle.RegisterUnmarshalFunc(cfg.FormatBundleFile, cfg.UnmarshalFunc) cfg.bundle = bundle cfg.loadMessages() cfg.initLocalizerMap() return &cfg } func (c *Config) loadMessage(filepath string) { buf, err := c.Loader.LoadMessage(filepath) if err != nil { panic(err) } if _, err := c.bundle.ParseMessageFileBytes(buf, filepath); err != nil { panic(err) } } func (c *Config) loadMessages() *Config { for _, lang := range c.AcceptLanguages { bundleFilePath := fmt.Sprintf("%s.%s", lang.String(), c.FormatBundleFile) filepath := path.Join(c.RootPath, bundleFilePath) c.loadMessage(filepath) } return c } func (c *Config) initLocalizerMap() { localizerMap := &sync.Map{} for _, lang := range c.AcceptLanguages { s := lang.String() localizerMap.Store(s, i18n.NewLocalizer(c.bundle, s)) } lang := c.DefaultLanguage.String() if _, ok := localizerMap.Load(lang); !ok { localizerMap.Store(lang, i18n.NewLocalizer(c.bundle, lang)) } c.localizerMap = localizerMap } /* MustLocalize get the i18n message without error handling param is one of these type: messageID, *i18n.LocalizeConfig Example: MustLocalize(ctx, "hello") // messageID is hello MustLocalize(ctx, &i18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": context.Param("name"), }, }) */ func (i *I18n) MustLocalize(ctx fiber.Ctx, params interface{}) string { message, err := i.Localize(ctx, params) if err != nil { panic(err) } return message } /* Localize get the i18n message param is one of these type: messageID, *i18n.LocalizeConfig Example: Localize(ctx, "hello") // messageID is hello Localize(ctx, &i18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": context.Param("name"), }, }) */ func (i *I18n) Localize(ctx fiber.Ctx, params interface{}) (string, error) { if i == nil || i.cfg == nil { return "", fmt.Errorf("i18n.Localize error: %v", "translator is nil") } appCfg := i.cfg lang := appCfg.LangHandler(ctx, appCfg.DefaultLanguage.String()) localizer, _ := appCfg.localizerMap.Load(lang) if localizer == nil { defaultLang := appCfg.DefaultLanguage.String() localizer, _ = appCfg.localizerMap.Load(defaultLang) } var localizeConfig *i18n.LocalizeConfig switch paramValue := params.(type) { case string: localizeConfig = &i18n.LocalizeConfig{MessageID: paramValue} case *i18n.LocalizeConfig: if paramValue == nil { return "", fmt.Errorf("i18n.Localize error: %v", "params is nil") } localizeConfig = paramValue default: return "", fmt.Errorf("i18n.Localize error: %v", "unsupported params type") } if localizer == nil { return "", fmt.Errorf("i18n.Localize error: %v", "localizer is nil") } loc, ok := localizer.(*i18n.Localizer) if !ok { return "", fmt.Errorf("i18n.Localize error: %v", "unexpected localizer type") } message, err := loc.Localize(localizeConfig) if err != nil { log.Errorf("i18n.Localize error: %v", err) return "", fmt.Errorf("i18n.Localize error: %v", err) } return message, nil } ================================================ FILE: v3/i18n/i18n_test.go ================================================ package i18n import ( "context" "fmt" "io" "net/http" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) func newServer(translator *I18n) *fiber.App { app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendString(translator.MustLocalize(ctx, "welcome")) }) app.Get("/:name", func(ctx fiber.Ctx) error { return ctx.SendString(translator.MustLocalize(ctx, &i18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": ctx.Params("name"), }, })) }) return app } var ( sharedTranslator = New() i18nApp = newServer(sharedTranslator) ) func makeRequest(lang language.Tag, name string, app *fiber.App) (*http.Response, error) { path := "/" + name req, _ := http.NewRequestWithContext(context.Background(), "GET", path, nil) req.Host = "localhost" if lang != language.Und { req.Header.Add("Accept-Language", lang.String()) } req.Method = "GET" req.RequestURI = path resp, err := app.Test(req) return resp, err } func TestI18nEN(t *testing.T) { t.Parallel() type args struct { lang language.Tag name string } tests := []struct { name string args args want string }{ { name: "hello world", args: args{ name: "", lang: language.English, }, want: "hello", }, { name: "hello alex", args: args{ name: "alex", lang: language.English, }, want: "hello alex", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := makeRequest(tt.args.lang, tt.args.name, i18nApp) assert.Equal(t, err, nil) body, err := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, err, nil) assert.Equal(t, tt.want, string(body)) }) } } func TestI18nZH(t *testing.T) { type args struct { lang language.Tag name string } tests := []struct { name string args args want string }{ { name: "hello world", args: args{ name: "", lang: language.Chinese, }, want: "你好", }, { name: "hello alex", args: args{ name: "alex", lang: language.Chinese, }, want: "你好 alex", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := makeRequest(tt.args.lang, tt.args.name, i18nApp) assert.Equal(t, err, nil) body, err := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, err, nil) assert.Equal(t, tt.want, string(body)) }) } } func TestParallelI18n(t *testing.T) { type args struct { lang language.Tag name string } tests := []struct { name string args args want string }{ { name: "hello world", args: args{ name: "", lang: language.Chinese, }, want: "你好", }, { name: "hello alex", args: args{ name: "alex", lang: language.Chinese, }, want: "你好 alex", }, { name: "hello peter", args: args{ name: "peter", lang: language.English, }, want: "hello peter", }, } t.Parallel() for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := makeRequest(tt.args.lang, tt.args.name, i18nApp) assert.Equal(t, err, nil) body, err := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, err, nil) assert.Equal(t, tt.want, string(body)) }) } } func TestTranslatorConcurrentLocalize(t *testing.T) { t.Parallel() const workers = 64 var wg sync.WaitGroup errCh := make(chan error, workers) for i := 0; i < workers; i++ { lang := language.English if i%2 == 1 { lang = language.Chinese } name := fmt.Sprintf("user-%d", i) wg.Add(1) go func(lang language.Tag, name string) { defer wg.Done() resp, err := makeRequest(lang, name, i18nApp) if err != nil { errCh <- err return } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { errCh <- err return } expected := fmt.Sprintf("hello %s", name) if lang == language.Chinese { expected = fmt.Sprintf("你好 %s", name) } if string(body) != expected { errCh <- fmt.Errorf("unexpected body %q for lang %s", string(body), lang.String()) } }(lang, name) } wg.Wait() close(errCh) for err := range errCh { assert.NoError(t, err) } } func TestLocalize(t *testing.T) { t.Parallel() translator := New() app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { localize, err := translator.Localize(ctx, "welcome?") assert.Equal(t, "", localize) return fiber.NewError(500, err.Error()) }) app.Get("/:name", func(ctx fiber.Ctx) error { name := ctx.Params("name") localize, err := translator.Localize(ctx, &i18n.LocalizeConfig{ MessageID: "welcomeWithName", TemplateData: map[string]string{ "name": name, }, }) assert.Equal(t, nil, err) return ctx.SendString(localize) }) t.Run("test localize", func(t *testing.T) { got, err := makeRequest(language.Chinese, "", app) assert.Equal(t, 500, got.StatusCode) assert.Equal(t, nil, err) body, _ := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, `i18n.Localize error: message "welcome?" not found in language "zh"`, string(body)) got, err = makeRequest(language.English, "name", app) assert.Equal(t, 200, got.StatusCode) assert.Equal(t, nil, err) body, _ = io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "hello name", string(body)) }) } func TestNew_doesNotMutateCallerConfig(t *testing.T) { t.Parallel() cfg := &Config{ RootPath: "./example/localize", AcceptLanguages: []language.Tag{language.Chinese, language.English}, } // DefaultLanguage is intentionally left as zero value (language.Und) // to verify that New() does not mutate the caller's config. originalDefaultLanguage := cfg.DefaultLanguage translator := New(cfg) assert.NotNil(t, translator) assert.Equal(t, originalDefaultLanguage, cfg.DefaultLanguage, "New() must not mutate the caller's Config") assert.Equal(t, language.Und, cfg.DefaultLanguage, "caller's DefaultLanguage should remain Und") } func TestLocalize_nilReceiver(t *testing.T) { t.Parallel() var translator *I18n localize, err := translator.Localize(nil, "welcome") assert.Equal(t, "", localize) assert.EqualError(t, err, "i18n.Localize error: translator is nil") } func TestLocalize_unsupportedParamsType(t *testing.T) { t.Parallel() translator := New() app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { localize, err := translator.Localize(ctx, 42) assert.Equal(t, "", localize) return fiber.NewError(500, err.Error()) }) got, err := makeRequest(language.English, "", app) assert.NoError(t, err) assert.Equal(t, 500, got.StatusCode) body, _ := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "i18n.Localize error: unsupported params type", string(body)) } func TestLocalize_nilLocalizeConfig(t *testing.T) { t.Parallel() translator := New() app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { localize, err := translator.Localize(ctx, (*i18n.LocalizeConfig)(nil)) assert.Equal(t, "", localize) return fiber.NewError(500, err.Error()) }) got, err := makeRequest(language.English, "", app) assert.NoError(t, err) assert.Equal(t, 500, got.StatusCode) body, _ := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "i18n.Localize error: params is nil", string(body)) } func TestMustLocalize_panics(t *testing.T) { t.Parallel() translator := New() app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { assert.Panics(t, func() { translator.MustLocalize(ctx, "nonexistent_message") }) return nil }) _, err := makeRequest(language.English, "", app) assert.NoError(t, err) } func Test_defaultLangHandler(t *testing.T) { app := fiber.New() app.Get("/", func(c fiber.Ctx) error { return c.SendString(defaultLangHandler(nil, language.English.String())) }) app.Get("/test", func(c fiber.Ctx) error { return c.SendString(defaultLangHandler(c, language.English.String())) }) t.Parallel() t.Run("test nil ctx", func(t *testing.T) { var wg sync.WaitGroup want := 100 wg.Add(want) for i := 0; i < want; i++ { go func() { defer wg.Done() got, err := makeRequest(language.English, "", app) assert.Equal(t, nil, err) body, _ := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "en", string(body)) }() } wg.Wait() }) t.Run("test query and header", func(t *testing.T) { got, err := makeRequest(language.Chinese, "test?lang=en", app) assert.Equal(t, nil, err) body, _ := io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "en", string(body)) got, err = makeRequest(language.Chinese, "test", app) assert.Equal(t, nil, err) body, _ = io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "zh", string(body)) got, err = makeRequest(language.Chinese, "test", app) assert.Equal(t, nil, err) body, _ = io.ReadAll(got.Body) got.Body.Close() assert.Equal(t, "zh", string(body)) }) } ================================================ FILE: v3/jwt/README.md ================================================ --- id: jwt --- # JWT ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*jwt*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20jwt/badge.svg) JWT returns a JSON Web Token (JWT) auth middleware. For valid token, it sets the token in Ctx.Locals (and in the underlying `context.Context` when `PassLocalsToContext` is enabled) and calls next handler. For invalid token, it returns "401 - Unauthorized" error. For missing token, it returns "400 - Bad Request" error. Special thanks and credits to [Echo](https://echo.labstack.com/middleware/jwt) **Compatible with Fiber v3.** ## 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. ## Install ```bash go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/jwt go get -u github.com/golang-jwt/jwt/v5 ``` ## Signature ```go jwtware.New(config ...jwtware.Config) func(fiber.Ctx) error jwtware.FromContext(ctx any) *jwt.Token // jwt "github.com/golang-jwt/jwt/v5" ``` `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`. ## Config | Property | Type | Description | Default | |:-------------------|:-------------------------------------|:-------------------------------------------------------------------------------------------------------------|:-----------------------------| | Next | `func(fiber.Ctx) bool` | Defines a function to skip this middleware when it returns true | `nil` | | SuccessHandler | `func(fiber.Ctx) error` | Executed when a token is valid. | `c.Next()` | | ErrorHandler | `func(fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` | | SigningKey | `SigningKey` | Signing key used to validate the token. Used as a fallback if `SigningKeys` is empty. | `nil` | | SigningKeys | `map[string]SigningKey` | Map of signing keys used to validate tokens via the `kid` header. | `nil` | | Claims | `jwt.Claims` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` | | Extractor | `Extractor` | Function used to extract the token from the request. | `FromAuthHeader("Bearer")` | | TokenProcessorFunc | `func(token string) (string, error)` | TokenProcessorFunc processes the token extracted using the Extractor. | `nil` | | KeyFunc | `jwt.Keyfunc` | User-defined function that supplies the public key for token validation. | `nil` (uses internal default)| | JWKSetURLs | `[]string` | List of JSON Web Key (JWK) Set URLs used to obtain signing keys for parsing JWTs. | `nil` | ## Available Extractors JWT middleware uses the shared Fiber extractors (github.com/gofiber/fiber/v3/extractors) and provides several helpers for different token sources. Import them with: ```go import "github.com/gofiber/fiber/v3/extractors" ``` For an overview and additional examples, see the Fiber Extractors guide: - https://docs.gofiber.io/guide/extractors - `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.** - `extractors.FromHeader(header string)` - Extracts token from the specified HTTP header - `extractors.FromQuery(param string)` - Extracts token from URL query parameters - `extractors.FromParam(param string)` - Extracts token from URL path parameters - `extractors.FromCookie(key string)` - Extracts token from cookies - `extractors.FromForm(param string)` - Extracts token from form data - `extractors.Chain(extrs ...extractors.Extractor)` - Tries multiple extractors in order until one succeeds ### Security Considerations ⚠️ **Security Warning**: When choosing an extractor, consider the security implications: - **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. - **Form-based extractors** (`FromForm`): Similar risks to URL extractors, especially if forms are submitted via GET requests. - **Header-based extractors** (`FromAuthHeader`, `FromHeader`): Most secure as headers are not typically logged or exposed in referrers. - **Cookie-based extractors** (`FromCookie`): Secure for web applications but requires proper cookie security settings (HttpOnly, Secure, SameSite). **Recommendation**: Use `FromAuthHeader("Bearer")` (the default) for production applications unless you have specific requirements that necessitate alternative extractors. ## HS256 Example ```go package main import ( "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" jwtware "github.com/gofiber/contrib/v3/jwt" "github.com/golang-jwt/jwt/v5" ) func main() { app := fiber.New() // Login route app.Post("/login", login) // Unauthenticated route app.Get("/", accessible) // JWT Middleware app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte("secret")}, Extractor: extractors.FromAuthHeader("Bearer"), })) // Restricted Routes app.Get("/restricted", restricted) app.Listen(":3000") } func login(c fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") // Throws Unauthorized error if user != "john" || pass != "doe" { return c.SendStatus(fiber.StatusUnauthorized) } // Create the Claims claims := jwt.MapClaims{ "name": "John Doe", "admin": true, "exp": time.Now().Add(time.Hour * 72).Unix(), } // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Generate encoded token and send it as response. t, err := token.SignedString([]byte("secret")) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(fiber.Map{"token": t}) } func accessible(c fiber.Ctx) error { return c.SendString("Accessible") } func restricted(c fiber.Ctx) error { user := jwtware.FromContext(c) claims := user.Claims.(jwt.MapClaims) name := claims["name"].(string) return c.SendString("Welcome " + name) } ``` ## Cookie Extractor Example ```go package main import ( "github.com/gofiber/fiber/v3" jwtware "github.com/gofiber/contrib/v3/jwt" ) func main() { app := fiber.New() // JWT Middleware with cookie extractor app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte("secret")}, Extractor: extractors.FromCookie("token"), })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("Protected route") }) app.Listen(":3000") } ``` ## HS256 Test _Login using username and password to retrieve a token._ ```bash curl --data "user=john&pass=doe" http://localhost:3000/login ``` _Response_ ```json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" } ``` _Request a restricted resource using the token in Authorization request header._ ```bash curl localhost:3000/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" ``` _Response_ ```text Welcome John Doe ``` ## RS256 Example ```go package main import ( "crypto/rand" "crypto/rsa" "log" "time" "github.com/gofiber/fiber/v3" "github.com/golang-jwt/jwt/v5" jwtware "github.com/gofiber/contrib/v3/jwt" ) var ( // Obviously, this is just a test example. Do not do this in production. // In production, you would have the private key and public key pair generated // in advance. NEVER add a private key to any GitHub repo. privateKey *rsa.PrivateKey ) func main() { app := fiber.New() // Just as a demo, generate a new private/public key pair on each run. See note above. rng := rand.Reader var err error privateKey, err = rsa.GenerateKey(rng, 2048) if err != nil { log.Fatalf("rsa.GenerateKey: %v", err) } // Login route app.Post("/login", login) // Unauthenticated route app.Get("/", accessible) // JWT Middleware app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: jwtware.RS256, Key: privateKey.Public(), }, Extractor: extractors.FromAuthHeader("Bearer"), })) // Restricted Routes app.Get("/restricted", restricted) app.Listen(":3000") } func login(c fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") // Throws Unauthorized error if user != "john" || pass != "doe" { return c.SendStatus(fiber.StatusUnauthorized) } // Create the Claims claims := jwt.MapClaims{ "name": "John Doe", "admin": true, "exp": time.Now().Add(time.Hour * 72).Unix(), } // Create token token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) // Generate encoded token and send it as response. t, err := token.SignedString(privateKey) if err != nil { log.Printf("token.SignedString: %v", err) return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(fiber.Map{"token": t}) } func accessible(c fiber.Ctx) error { return c.SendString("Accessible") } func restricted(c fiber.Ctx) error { user := jwtware.FromContext(c) claims := user.Claims.(jwt.MapClaims) name := claims["name"].(string) return c.SendString("Welcome " + name) } ``` ## Retrieving the token with PassLocalsToContext When `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: ```go // From a fiber.Ctx (most common usage) token := jwtware.FromContext(c) // From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled) token := jwtware.FromContext(c.Context()) ``` ## RS256 Test The RS256 is actually identical to the HS256 test above. ## JWK Set Test The 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). ## Custom KeyFunc example KeyFunc defines a user-defined function that supplies the public key for a token validation. The function shall take care of verifying the signing algorithm and selecting the proper key. A user-defined KeyFunc can be useful if tokens are issued by an external party. When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. This is one of the three options to provide a token validation key. The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. Required if neither SigningKeys nor SigningKey is provided. Default to an internal implementation verifying the signing algorithm and selecting the proper key. ```go package main import ( "fmt" "github.com/gofiber/fiber/v3" jwtware "github.com/gofiber/contrib/v3/jwt" "github.com/golang-jwt/jwt/v5" ) func main() { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ KeyFunc: customKeyFunc(), Extractor: extractors.FromAuthHeader("Bearer"), })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) } func customKeyFunc() jwt.Keyfunc { return func(t *jwt.Token) (interface{}, error) { // Always check the signing method if t.Method.Alg() != jwtware.HS256 { return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) } // TODO custom implementation of loading signing key like from a database signingKey := "secret" return []byte(signingKey), nil } } ``` ================================================ FILE: v3/jwt/config.go ================================================ package jwtware import ( "errors" "fmt" "log" "net/url" "time" "github.com/MicahParks/keyfunc/v2" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" "github.com/golang-jwt/jwt/v5" ) var ( // ErrJWTAlg is returned when the JWT header did not contain the expected algorithm. ErrJWTAlg = errors.New("the JWT header did not contain the expected algorithm") // ErrMissingToken is returned when no JWT token is found in the request. ErrMissingToken = errors.New("missing or malformed JWT") ) // Config defines the config for JWT middleware type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(fiber.Ctx) bool // SuccessHandler is executed when a token is successfully validated. // Optional. Default: nil SuccessHandler fiber.Handler // ErrorHandler is executed when token validation fails. // It allows customization of JWT error responses. // Optional. Default: 401 Invalid or expired JWT ErrorHandler fiber.ErrorHandler // SigningKey is the primary key used to validate tokens. // Used as a fallback if SigningKeys is empty. // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. SigningKey SigningKey // SigningKeys is a map of keys used to validate tokens with the "kid" field. // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. SigningKeys map[string]SigningKey // Claims are extendable claims data defining token content. // Optional. Default value jwt.MapClaims Claims jwt.Claims // Extractor defines a function to extract the token from the request. // Optional. Default: FromAuthHeader("Bearer"). Extractor extractors.Extractor // TokenProcessorFunc processes the token extracted using the Extractor. // Optional. Default: nil TokenProcessorFunc func(token string) (string, error) // KeyFunc provides the public key for JWT verification. // It handles algorithm verification and key selection. // By default, the github.com/MicahParks/keyfunc/v2 package is used. // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. KeyFunc jwt.Keyfunc // JWKSetURLs is a list of URLs containing JSON Web Key Sets (JWKS) for signature verification. // HTTPS is recommended. The "kid" field in the JWT header and JWKs is mandatory. // Default behavior: // - Refresh every hour. // - Auto-refresh on new "kid" in JWT. // - Rate limit refreshes to once every 5 minutes. // - Timeout refreshes after 10 seconds. // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. JWKSetURLs []string } // SigningKey holds information about the recognized cryptographic keys used to sign JWTs by this program. type SigningKey struct { // JWTAlg is the algorithm used to sign JWTs. If this value is a non-empty string, this will be checked against the // "alg" value in the JWT header. // // https://www.rfc-editor.org/rfc/rfc7518#section-3.1 JWTAlg string // Key is the cryptographic key used to sign JWTs. For supported types, please see // https://github.com/golang-jwt/jwt. Key interface{} } // makeCfg function will check correctness of supplied configuration // and will complement it with default values instead of missing ones func makeCfg(config []Config) (cfg Config) { if len(config) > 0 { cfg = config[0] } if cfg.SuccessHandler == nil { cfg.SuccessHandler = func(c fiber.Ctx) error { return c.Next() } } if cfg.ErrorHandler == nil { cfg.ErrorHandler = func(c fiber.Ctx, err error) error { if errors.Is(err, extractors.ErrNotFound) { return c.Status(fiber.StatusBadRequest).SendString(ErrMissingToken.Error()) } if e, ok := err.(*fiber.Error); ok { return c.Status(e.Code).SendString(e.Message) } return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT") } } if cfg.SigningKey.Key == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil { panic("Fiber: JWT middleware configuration: At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.") } if len(cfg.SigningKeys) > 0 { for _, key := range cfg.SigningKeys { if key.Key == nil { panic("Fiber: JWT middleware configuration: SigningKey.Key cannot be nil") } } } if len(cfg.JWKSetURLs) > 0 { for _, u := range cfg.JWKSetURLs { parsed, err := url.Parse(u) if err != nil || parsed.Scheme == "" || parsed.Host == "" { panic("Fiber: JWT middleware configuration: Invalid JWK Set URL (must be absolute http/https): " + u) } if parsed.Scheme != "https" && parsed.Scheme != "http" { panic("Fiber: JWT middleware configuration: Unsupported JWK Set URL scheme: " + parsed.Scheme) } } } if cfg.Claims == nil { cfg.Claims = jwt.MapClaims{} } if cfg.Extractor.Extract == nil { cfg.Extractor = extractors.FromAuthHeader("Bearer") } if cfg.KeyFunc == nil { if len(cfg.SigningKeys) > 0 || len(cfg.JWKSetURLs) > 0 { var givenKeys map[string]keyfunc.GivenKey if cfg.SigningKeys != nil { givenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys)) for kid, key := range cfg.SigningKeys { givenKeys[kid] = keyfunc.NewGivenCustom(key.Key, keyfunc.GivenKeyOptions{ Algorithm: key.JWTAlg, }) } } if len(cfg.JWKSetURLs) > 0 { var err error cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) if err != nil { panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) } } else { cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc } } else { cfg.KeyFunc = signingKeyFunc(cfg.SigningKey) } } return cfg } func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) { opts := keyfuncOptions(givenKeys) multiple := make(map[string]keyfunc.Options, len(jwkSetURLs)) for _, url := range jwkSetURLs { multiple[url] = opts } multiOpts := keyfunc.MultipleOptions{ KeySelector: keyfunc.KeySelectorFirst, } multi, err := keyfunc.GetMultiple(multiple, multiOpts) if err != nil { return nil, fmt.Errorf("failed to get multiple JWK Set URLs: %w", err) } return multi.Keyfunc, nil } func keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options { return keyfunc.Options{ GivenKeys: givenKeys, RefreshErrorHandler: func(err error) { log.Printf("Failed to perform background refresh of JWK Set: %s.", err) }, RefreshInterval: time.Hour, RefreshRateLimit: time.Minute * 5, RefreshTimeout: time.Second * 10, RefreshUnknownKID: true, } } func signingKeyFunc(key SigningKey) jwt.Keyfunc { return func(token *jwt.Token) (interface{}, error) { if key.JWTAlg != "" { alg, ok := token.Header["alg"].(string) if !ok { return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: missing or unexpected JSON type", key.JWTAlg) } if alg != key.JWTAlg { return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: %q", key.JWTAlg, alg) } } return key.Key, nil } } ================================================ FILE: v3/jwt/config_test.go ================================================ package jwtware import ( "fmt" "testing" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/require" ) func TestPanicOnMissingConfiguration(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err == nil { t.Fatalf("Middleware should panic on missing configuration") } }() // Arrange config := make([]Config, 0) // Act makeCfg(config) } func TestDefaultConfiguration(t *testing.T) { t.Parallel() // Arrange config := append(make([]Config, 0), Config{ SigningKey: SigningKey{Key: []byte("")}, }) // Act cfg := makeCfg(config) // Assert require.NotNil(t, cfg.Claims, "Default claims should not be 'nil'") require.Equal(t, extractors.SourceAuthHeader, cfg.Extractor.Source, "Default extractor source should be '%v'", extractors.SourceAuthHeader) require.Equal(t, fiber.HeaderAuthorization, cfg.Extractor.Key, "Default extractor key should be '%v'", fiber.HeaderAuthorization) require.Equal(t, "Bearer", cfg.Extractor.AuthScheme, "Default auth scheme should be 'Bearer'") } func TestCustomExtractor(t *testing.T) { t.Parallel() // Arrange extractor := extractors.FromHeader("X-Auth-Token") config := append(make([]Config, 0), Config{ SigningKey: SigningKey{Key: []byte("")}, Extractor: extractor, }) // Act cfg := makeCfg(config) // Assert require.Equal(t, extractor.Source, cfg.Extractor.Source, "Extractor source should be the custom one") require.Equal(t, extractor.Key, cfg.Extractor.Key, "Extractor key should be the custom one") require.Equal(t, "", cfg.Extractor.AuthScheme, "AuthScheme should be empty for non-Authorization extractors") } func TestPanicOnInvalidSigningKey(t *testing.T) { t.Parallel() config := append(make([]Config, 0), Config{ SigningKey: SigningKey{Key: nil}, // Invalid key }) require.Panics(t, func() { makeCfg(config) }) } func TestPanicOnInvalidSigningKeys(t *testing.T) { t.Parallel() config := append(make([]Config, 0), Config{ SigningKeys: map[string]SigningKey{ "key1": {Key: nil}, // Invalid key }, }) require.Panics(t, func() { makeCfg(config) }) } func TestPanicOnInvalidJWKSetURLs(t *testing.T) { t.Parallel() // Arrange config := append(make([]Config, 0), Config{ JWKSetURLs: []string{"invalid-url"}, // This would cause panic in keyfunc }) require.Panics(t, func() { makeCfg(config) }) } func TestCustomClaims(t *testing.T) { t.Parallel() // Arrange customClaims := jwt.MapClaims{"custom": "claims"} config := append(make([]Config, 0), Config{ SigningKey: SigningKey{Key: []byte("")}, Claims: customClaims, }) // Act cfg := makeCfg(config) // Assert require.NotNil(t, cfg.Claims, "Custom claims should be preserved") // Check if it's the same map by checking a key claimsMap, ok := cfg.Claims.(jwt.MapClaims) require.True(t, ok, "Claims should be MapClaims") require.Equal(t, "claims", claimsMap["custom"], "Custom claims content should be preserved") } func TestTokenProcessorFunc_Configured(t *testing.T) { t.Parallel() // Arrange config := append(make([]Config, 0), Config{ SigningKey: SigningKey{Key: []byte("")}, TokenProcessorFunc: func(token string) (string, error) { return "", fmt.Errorf("processing failed") }, }) // Act cfg := makeCfg(config) // Assert require.NotNil(t, cfg.TokenProcessorFunc, "TokenProcessorFunc should be set") // Exercise the processor _, err := cfg.TokenProcessorFunc("dummy") require.Error(t, err, "TokenProcessorFunc should return error") } func TestPanicOnUnsupportedJWKSetURLScheme(t *testing.T) { t.Parallel() config := append(make([]Config, 0), Config{ JWKSetURLs: []string{"ftp://example.com"}, // Unsupported scheme }) require.Panics(t, func() { makeCfg(config) }) } ================================================ FILE: v3/jwt/crypto.go ================================================ package jwtware const ( // HS256 represents a public cryptography key generated by a 256 bit HMAC algorithm. HS256 = "HS256" // HS384 represents a public cryptography key generated by a 384 bit HMAC algorithm. HS384 = "HS384" // HS512 represents a public cryptography key generated by a 512 bit HMAC algorithm. HS512 = "HS512" // ES256 represents a public cryptography key generated by a 256 bit ECDSA algorithm. ES256 = "ES256" // ES384 represents a public cryptography key generated by a 384 bit ECDSA algorithm. ES384 = "ES384" // ES512 represents a public cryptography key generated by a 512 bit ECDSA algorithm. ES512 = "ES512" // P256 represents a cryptographic elliptical curve type. P256 = "P-256" // P384 represents a cryptographic elliptical curve type. P384 = "P-384" // P521 represents a cryptographic elliptical curve type. P521 = "P-521" // RS256 represents a public cryptography key generated by a 256 bit RSA algorithm. RS256 = "RS256" // RS384 represents a public cryptography key generated by a 384 bit RSA algorithm. RS384 = "RS384" // RS512 represents a public cryptography key generated by a 512 bit RSA algorithm. RS512 = "RS512" // PS256 represents a public cryptography key generated by a 256 bit RSA algorithm. PS256 = "PS256" // PS384 represents a public cryptography key generated by a 384 bit RSA algorithm. PS384 = "PS384" // PS512 represents a public cryptography key generated by a 512 bit RSA algorithm. PS512 = "PS512" ) ================================================ FILE: v3/jwt/go.mod ================================================ module github.com/gofiber/contrib/v3/jwt go 1.25.0 require ( github.com/MicahParks/keyfunc/v2 v2.1.0 github.com/gofiber/fiber/v3 v3.1.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/stretchr/testify v1.11.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/jwt/go.sum ================================================ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/jwt/jwt.go ================================================ // 🚀 Fiber is an Express inspired web framework written in Go with 💖 // 📌 API Documentation: https://fiber.wiki // 📝 Github Repository: https://github.com/gofiber/fiber // Special thanks to Echo: https://github.com/labstack/echo/blob/master/middleware/jwt.go package jwtware import ( "reflect" "github.com/gofiber/fiber/v3" "github.com/golang-jwt/jwt/v5" ) // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. type contextKey int // The following contextKey values are defined to store values in context. const ( tokenKey contextKey = iota ) // New ... func New(config ...Config) fiber.Handler { cfg := makeCfg(config) // Return middleware handler return func(c fiber.Ctx) error { // Filter request to skip middleware if cfg.Next != nil && cfg.Next(c) { return c.Next() } auth, err := cfg.Extractor.Extract(c) if err != nil { return cfg.ErrorHandler(c, err) } if cfg.TokenProcessorFunc != nil { auth, err = cfg.TokenProcessorFunc(auth) if err != nil { return cfg.ErrorHandler(c, err) } } var token *jwt.Token if _, ok := cfg.Claims.(jwt.MapClaims); ok { token, err = jwt.Parse(auth, cfg.KeyFunc) } else { t := reflect.ValueOf(cfg.Claims).Type().Elem() claims := reflect.New(t).Interface().(jwt.Claims) token, err = jwt.ParseWithClaims(auth, claims, cfg.KeyFunc) } if err == nil && token.Valid { // Store user information from token into context. fiber.StoreInContext(c, tokenKey, token) return cfg.SuccessHandler(c) } return cfg.ErrorHandler(c, err) } } // FromContext returns the token from the context. // It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context. // If there is no token, nil is returned. func FromContext(ctx any) *jwt.Token { token, ok := fiber.ValueFromContext[*jwt.Token](ctx, tokenKey) if !ok { return nil } return token } ================================================ FILE: v3/jwt/jwt_test.go ================================================ package jwtware_test import ( "encoding/hex" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" "github.com/golang-jwt/jwt/v5" jwtware "github.com/gofiber/contrib/v3/jwt" ) type TestToken struct { SigningMethod string Token string } var ( hamac = []TestToken{ { SigningMethod: jwtware.HS256, Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o", }, { SigningMethod: jwtware.HS384, Token: "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hO2sthNQUSfvI9ylUdMKDxcrm8jB3KL6Rtkd3FOskL-jVqYh2CK1es8FKCQO8_tW", }, { SigningMethod: jwtware.HS512, Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wUVS6tazE2N98_J4SH_djkEe1igXPu0qILAvVXCiO6O20gdf5vZ2sYFWX3c-Hy6L4TD47b3DSAAO9XjSqpJfag", }, } rsa = []TestToken{ { SigningMethod: jwtware.RS256, Token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.gvWLzl1sYUXdYqAPqFYLEJYtqPce8YxrV6LPiyWX2147llj1YfquFySnC8KOUTykCAxZHe6tFkyyZOp35HOqV3P-jxW2rw05mpNhld79f-O2sAFEzV7qxJXuYi4TL-Qn1gaLWP7i9B6B9c-0xLzYUmtLdrmlM2pxfPkXwG0oSao", }, { SigningMethod: jwtware.RS384, Token: "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.IIFu5jNRT5fIe91we3ARLTpE8hGu4tK6gsWtrJ1lAWzCxUYsVE02yOi3ya9RJsh-37GN8LdfVw74ZQzr4dwuq8SorycVatA2bc_OfkWpioOoPCqGMBFgsEdue0qtL1taflA-YSNG-Qntpqx_ciCGfI1DhiqikLaL-LSe8H9YOWk", }, { SigningMethod: jwtware.RS512, Token: "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.DKY-VXa6JJUZpupEUcmXETwaV2jfLydyeBfhSP8pIEW9g52fQ3g5hrHCNstxG2yy9yU68yrFqrBDetDX_yJ6qSHAOInwGWYot8W4D0lJvqsHJe0W0IPi03xiaWjwKO26xENCUzNNLvSPKPox5DPcg31gzCFBrIUgVX-TkpajuSE", }, } ecdsa = []TestToken{ { SigningMethod: jwtware.ES256, Token: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC0yNTYifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.n6iJptkq2i6Y6gbuc92f2ExT9oXbg7hdMlR5MvkCZjayxBAyfpIGGoQAjMriwEs4rjF5F-DSU8T6eUcDxNhonA", }, { SigningMethod: jwtware.ES384, Token: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC0zODQifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.WYGFC6NTSzD1E3Zv7Lyy3m_1l0zoF2tZqvDBxQBXqJN-bStTBzNYnpWZDMN6XMI7OqFbPGlh_Jff4Z4dlf0bieEfenURdtpoLIQI1zPNXoIfaY7TH8BTAXQKtoBk89Ed", }, { SigningMethod: jwtware.ES512, Token: "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC01MjEifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ADwlteggILiCM_oCkxsyJTRK6BpQyH2FBQD_Tw_ph0vpLPRrpAkyh_CZIY9uZqqpb3J_eohscCzj5Vo9jrhP9DFRAdvLZCgehLj6N8P9aro2uy9jAl7kowxe0nEErv1SrD9qlyLWJh80jJVHRBVHXXysQ2WUD0KiRBq4x1p8jdEw5vHy", }, } ) const ( defaultSigningKey = "secret" defaultKeySet = ` { "keys":[ { "e": "AQAB", "kid": "gofiber-rsa", "kty": "RSA", "n": "2IPZysef6KVySrb_RPopuwWy1C7KRfE96zQ9jIRwPghlvs0yfj9VK4rqeYbuHp5k9ghbjm1Bn2LMLR-JzqYWbchxzVrV58ay4nRHYUSjyzdbNcG0J4W-NxHnVqK0UUOl59uikRDqGHh3eRen_jVO_B8lvhqM57HQhA-czHbsmeU" }, { "crv": "P-256", "kid": "gofiber-p-256", "kty": "EC", "x": "nLZJMz-8B6p2A1-owmTrCZqZx87_Y5soNPW74dQ8EDw", "y": "RvuLyi0tS-Tcx35IMy6aL_ID0K-cJFXmkFR8t9XJ4pc" }, { "crv": "P-384", "kid": "gofiber-p-384", "kty": "EC", "x": "wvSt-v7az1qbz493ToTSvNcXgdIGqTtlcLzW7B1Ko3QWVgmtBYWQr_Q311_QX9DY", "y": "DvvBgCVjsDyttGAF8cmTP5maV46PrxACZFLvC1OEiZh-Ul0obSGXqG2xu8ulINPy" }, { "crv": "P-521", "kid": "gofiber-p-521", "kty": "EC", "x": "AZhzdsnk9Dx5fLdPDnYJOI3ClkghbyFvpSq2ExzyPNgjZz_7iBUjyyLtr6QDn9BAaeFvSQFHvhZUylIQZ9wdIinq", "y": "AC2Me0tRqydVv7d23_0xdjiDndGuk0XpSZL5jeDWQ1_Tuty28-pJrFx38QQmWnosC0lBEdOUjxq-71YP7e4TzRMR" } ] } ` ) func TestJwtTokenProcessorFunc(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err != nil { t.Fatalf("Middleware should not panic") } }() for _, test := range hamac { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: test.SigningMethod, Key: []byte(defaultSigningKey), }, TokenProcessorFunc: func(token string) (string, error) { decodedToken, err := hex.DecodeString(token) return string(decodedToken), err }, })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+hex.EncodeToString([]byte(test.Token))) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } } func TestJwtFromHeader(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err != nil { t.Fatalf("Middleware should not panic") } }() t.Run("regular", func(t *testing.T) { for _, test := range hamac { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: test.SigningMethod, Key: []byte(defaultSigningKey), }, })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+test.Token) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } }) t.Run("custom", func(t *testing.T) { for _, test := range hamac { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: test.SigningMethod, Key: []byte(defaultSigningKey), }, Extractor: extractors.FromHeader("X-Token"), })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Set("X-Token", test.Token) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } }) t.Run("malformed header", func(t *testing.T) { for _, test := range hamac { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: test.SigningMethod, Key: []byte(defaultSigningKey), }, })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer"+test.Token) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 400, resp.StatusCode) } }) } func TestJwtFromCookie(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err != nil { t.Fatalf("Middleware should not panic") } }() for _, test := range hamac { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: test.SigningMethod, Key: []byte(defaultSigningKey), }, Extractor: extractors.FromCookie("Token"), })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) cookie := &http.Cookie{ Name: "Token", Value: test.Token, } req.AddCookie(cookie) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } } // TestJWKs performs a table test on the JWKs code. // deprecated func TestJwkFromServer(t *testing.T) { // Could add a test with an invalid JWKs endpoint. // Create a temporary directory to serve the JWKs from. tempDir, err := os.MkdirTemp("", "*") if err != nil { t.Errorf("Failed to create a temporary directory.\nError:%s\n", err.Error()) t.FailNow() } defer func() { if err = os.RemoveAll(tempDir); err != nil { t.Errorf("Failed to remove temporary directory.\nError:%s\n", err.Error()) t.FailNow() } }() // Create the JWKs file path. jwksFile := filepath.Join(tempDir, "jwks.json") // Write the empty JWKs. if err = os.WriteFile(jwksFile, []byte(defaultKeySet), 0600); err != nil { t.Errorf("Failed to write JWKs file to temporary directory.\nError:%s\n", err.Error()) t.FailNow() } // Create the HTTP test server. server := httptest.NewServer(http.FileServer(http.Dir(tempDir))) defer server.Close() // Iterate through the test cases. for _, test := range append(rsa, ecdsa...) { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ JWKSetURLs: []string{server.URL + "/jwks.json"}, })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+test.Token) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } } // TestJWKs performs a table test on the JWKs code. func TestJwkFromServers(t *testing.T) { // Could add a test with an invalid JWKs endpoint. // Create a temporary directory to serve the JWKs from. tempDir, err := os.MkdirTemp("", "*") if err != nil { t.Errorf("Failed to create a temporary directory.\nError:%s\n", err.Error()) t.FailNow() } defer func() { if err = os.RemoveAll(tempDir); err != nil { t.Errorf("Failed to remove temporary directory.\nError:%s\n", err.Error()) t.FailNow() } }() // Create the JWKs file path. jwksFile := filepath.Join(tempDir, "jwks.json") jwksFile2 := filepath.Join(tempDir, "jwks2.json") // Write the empty JWKs. if err = os.WriteFile(jwksFile, []byte(defaultKeySet), 0600); err != nil { t.Errorf("Failed to write JWKs file to temporary directory.\nError:%s\n", err.Error()) t.FailNow() } // Write the empty JWKs 2. if err = os.WriteFile(jwksFile2, []byte(defaultKeySet), 0600); err != nil { t.Errorf("Failed to write JWKs file to temporary directory.\nError:%s\n", err.Error()) t.FailNow() } // Create the HTTP test server. server := httptest.NewServer(http.FileServer(http.Dir(tempDir))) defer server.Close() // Iterate through the test cases. for _, test := range append(rsa, ecdsa...) { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ JWKSetURLs: []string{server.URL + "/jwks.json", server.URL + "/jwks2.json"}, })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+test.Token) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } } func TestCustomKeyfunc(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err != nil { t.Fatalf("Middleware should not panic") } }() test := hamac[0] // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ KeyFunc: customKeyfunc(), })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+test.Token) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } func TestMultiKeys(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err != nil { t.Fatalf("Middleware should not panic") } }() signMethod := jwt.SigningMethodHS512 keys := map[string]jwtware.SigningKey{ "1": { JWTAlg: signMethod.Name, Key: []byte("aaa"), }, "2": { JWTAlg: signMethod.Name, Key: []byte("bbb"), }, } testTokens := make(map[string]string) for kid, key := range keys { token := jwt.New(signMethod) token.Header["kid"] = kid tokenStr, err := token.SignedString(key.Key) if err != nil { t.Error(err) return } testTokens[kid] = tokenStr } // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKeys: keys, })) app.Get("/ok", func(c fiber.Ctx) error { return c.SendString("OK") }) for _, tokenStr := range testTokens { req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+tokenStr) // Act resp, err := app.Test(req) // Assert assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) } } func customKeyfunc() jwt.Keyfunc { return func(t *jwt.Token) (interface{}, error) { // Always check the signing method if t.Method.Alg() != jwtware.HS256 { return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) } return []byte(defaultSigningKey), nil } } func TestFromContext(t *testing.T) { t.Parallel() defer func() { // Assert if err := recover(); err != nil { t.Fatalf("Middleware should not panic") } }() for _, test := range hamac { // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{ JWTAlg: test.SigningMethod, Key: []byte(defaultSigningKey), }, })) app.Get("/ok", func(c fiber.Ctx) error { token := jwtware.FromContext(c) if token == nil { return c.SendStatus(fiber.StatusUnauthorized) } return c.SendString("OK") }) req := httptest.NewRequest("GET", "/ok", nil) req.Header.Add("Authorization", "Bearer "+test.Token) // Act resp, err := app.Test(req) // Assert assert.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } } func TestCustomErrorHandler(t *testing.T) { t.Parallel() // Arrange app := fiber.New() customErrorCalled := false app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)}, ErrorHandler: func(c fiber.Ctx, err error) error { customErrorCalled = true return c.Status(fiber.StatusTeapot).SendString("Custom Error: " + err.Error()) }, })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/protected", nil) req.Header.Add("Authorization", "Bearer invalid.token.here") // Act resp, err := app.Test(req) // Assert assert.NoError(t, err) assert.Equal(t, fiber.StatusTeapot, resp.StatusCode) b, _ := io.ReadAll(resp.Body) assert.Contains(t, string(b), "Custom Error:") assert.True(t, customErrorCalled) } func TestCustomSuccessHandler(t *testing.T) { t.Parallel() // Arrange app := fiber.New() customSuccessCalled := false app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)}, SuccessHandler: func(c fiber.Ctx) error { customSuccessCalled = true c.Locals("custom", "success") return c.Next() }, })) app.Get("/protected", func(c fiber.Ctx) error { if c.Locals("custom") == "success" { return c.SendString("Custom Success Handler Worked") } return c.SendString("OK") }) req := httptest.NewRequest("GET", "/protected", nil) req.Header.Add("Authorization", "Bearer "+hamac[0].Token) // Act resp, err := app.Test(req) // Assert assert.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) b, _ := io.ReadAll(resp.Body) assert.Equal(t, "Custom Success Handler Worked", string(b)) assert.True(t, customSuccessCalled) } func TestNextFunction(t *testing.T) { t.Parallel() // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)}, Next: func(c fiber.Ctx) bool { return c.Get("Skip-JWT") == "true" }, })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("Should not reach here") }) app.Get("/skipped", func(c fiber.Ctx) error { return c.SendString("Skipped JWT") }) // Test skipping JWT req := httptest.NewRequest("GET", "/skipped", nil) req.Header.Add("Skip-JWT", "true") resp, err := app.Test(req) assert.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) body, _ := io.ReadAll(resp.Body) assert.Equal(t, "Skipped JWT", string(body)) // Test not skipping JWT (should fail without token) req2 := httptest.NewRequest("GET", "/protected", nil) resp2, err2 := app.Test(req2) assert.NoError(t, err2) assert.Equal(t, 400, resp2.StatusCode) } func TestInvalidSigningKey(t *testing.T) { t.Parallel() assert.Panics(t, func() { app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: nil}, // Invalid key })) }, "Middleware should panic on invalid signing key") } func TestFromContextWithoutToken(t *testing.T) { t.Parallel() // Arrange app := fiber.New() app.Get("/no-jwt", func(c fiber.Ctx) error { token := jwtware.FromContext(c) if token == nil { return c.SendString("No token as expected") } return c.SendString("Unexpected token") }) req := httptest.NewRequest("GET", "/no-jwt", nil) // Act resp, err := app.Test(req) // Assert assert.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) } func TestMalformedToken(t *testing.T) { t.Parallel() // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)}, })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/protected", nil) req.Header.Add("Authorization", "Bearer not.a.jwt.token") // Act resp, err := app.Test(req) // Assert assert.NoError(t, err) assert.Equal(t, 401, resp.StatusCode) } func TestTokenProcessorFuncError(t *testing.T) { t.Parallel() // Arrange app := fiber.New() app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)}, TokenProcessorFunc: func(token string) (string, error) { return "", fiber.NewError(fiber.StatusBadRequest, "Token processing failed") }, })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("OK") }) req := httptest.NewRequest("GET", "/protected", nil) req.Header.Add("Authorization", "Bearer "+hamac[0].Token) // Act resp, err := app.Test(req) // Assert assert.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) } func TestFromContext_PassLocalsToContext(t *testing.T) { t.Parallel() app := fiber.New(fiber.Config{PassLocalsToContext: true}) app.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte(defaultSigningKey)}, })) app.Get("/ok", func(c fiber.Ctx) error { tokenFromCtx := jwtware.FromContext(c) tokenFromStdCtx := jwtware.FromContext(c.Context()) if tokenFromCtx == nil || tokenFromStdCtx == nil { return c.SendStatus(fiber.StatusUnauthorized) } return c.SendStatus(fiber.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "/ok", nil) req.Header.Add("Authorization", "Bearer "+hamac[0].Token) resp, err := app.Test(req) assert.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } ================================================ FILE: v3/loadshed/README.md ================================================ --- id: loadshed --- # LoadShed ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*loadshed*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20Loadshed/badge.svg) The LoadShed middleware for [Fiber](https://github.com/gofiber/fiber) is designed to help manage server load by shedding requests based on certain load criteria. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/loadshed ``` ## Signatures ```go loadshed.New(config ...loadshed.Config) fiber.Handler ``` ## Examples To use the LoadShed middleware in your Fiber application, import it and apply it to your Fiber app. Here's an example: ### Basic ```go package main import ( "time" "github.com/gofiber/fiber/v3" loadshed "github.com/gofiber/contrib/v3/loadshed" ) func main() { app := fiber.New() // Configure and use LoadShed middleware app.Use(loadshed.New(loadshed.Config{ Criteria: &loadshed.CPULoadCriteria{ LowerThreshold: 0.75, // Set your own lower threshold UpperThreshold: 0.90, // Set your own upper threshold Interval: 10 * time.Second, Getter: &loadshed.DefaultCPUPercentGetter{}, }, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Welcome!") }) app.Listen(":3000") } ``` ### With a custom rejection handler ```go package main import ( "time" "github.com/gofiber/fiber/v3" loadshed "github.com/gofiber/contrib/v3/loadshed" ) func main() { app := fiber.New() // Configure and use LoadShed middleware app.Use(loadshed.New(loadshed.Config{ Criteria: &loadshed.CPULoadCriteria{ LowerThreshold: 0.75, // Set your own lower threshold UpperThreshold: 0.90, // Set your own upper threshold Interval: 10 * time.Second, Getter: &loadshed.DefaultCPUPercentGetter{}, }, OnShed: func(ctx fiber.Ctx) error { if ctx.Method() == fiber.MethodGet { return ctx. Status(fiber.StatusTooManyRequests). Send([]byte{}) } return ctx. Status(fiber.StatusTooManyRequests). JSON(fiber.Map{ "error": "Keep calm", }) }, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Welcome!") }) app.Listen(":3000") } ``` ## Config The LoadShed middleware in Fiber offers various configuration options to tailor the load shedding behavior according to the needs of your application. | Property | Type | Description | Default | |:---------|:---------------------------|:--------------------------------------------------------|:------------------------| | Next | `func(fiber.Ctx) bool` | Function to skip this middleware when returned true. | `nil` | | Criteria | `LoadCriteria` | Interface for defining load shedding criteria. | `&CPULoadCriteria{...}` | | OnShed | `func(c fiber.Ctx) error` | Function to be executed if a request should be declined | `nil` | ## LoadCriteria LoadCriteria 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. ### CPULoadCriteria `CPULoadCriteria` is an implementation of the `LoadCriteria` interface, using CPU load as the metric for determining whether to shed requests. #### Properties | Property | Type | Description | |:---------------|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------| | 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. | | 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. | | Interval | `time.Duration` | The time interval over which the CPU usage is averaged for decision making. | | Getter | `CPUPercentGetter` | Interface to retrieve CPU usage percentages. | #### How It Works `CPULoadCriteria` determines the load on the system based on CPU usage and decides whether to shed incoming requests. It operates on the following principles: - **CPU Usage Measurement**: It measures the CPU usage over a specified interval. - **Thresholds**: Utilizes `LowerThreshold` and `UpperThreshold` values to decide when to start shedding requests. - **Proportional Rejection Probability**: - **Below `LowerThreshold`**: No requests are rejected, as the system is considered under acceptable load. - **Between `LowerThreshold` and `UpperThreshold`**: The probability of rejecting a request increases as the CPU usage approaches the `UpperThreshold`. This is calculated using the formula: ```plaintext rejectionProbability := (cpuUsage - LowerThreshold*100) / (UpperThreshold - LowerThreshold) ``` - **Above `UpperThreshold`**: All requests are rejected to prevent system overload. This mechanism ensures that the system can adaptively manage its load, maintaining stability and performance under varying traffic conditions. ## Default Config This is the default configuration for `LoadCriteria` in the LoadShed middleware. ```go var ConfigDefault = Config{ Next: nil, Criteria: &CPULoadCriteria{ LowerThreshold: 0.90, // 90% CPU usage as the start point for considering shedding UpperThreshold: 0.95, // 95% CPU usage as the point where all requests are shed Interval: 10 * time.Second, // CPU usage is averaged over 10 seconds Getter: &DefaultCPUPercentGetter{}, // Default method for getting CPU usage }, OnShed: nil, } ``` ================================================ FILE: v3/loadshed/cpu.go ================================================ package loadshed import ( "context" "fmt" "math" "math/rand" "sync" "sync/atomic" "time" "github.com/shirou/gopsutil/cpu" ) // LoadCriteria interface for different types of load metrics. type LoadCriteria interface { Metric(ctx context.Context) (float64, error) ShouldShed(metric float64) bool } // CPULoadCriteria for using CPU as a load metric. type CPULoadCriteria struct { LowerThreshold float64 UpperThreshold float64 Interval time.Duration Getter CPUPercentGetter once sync.Once cached atomic.Uint64 lastErr atomic.Value // stores error; nil means "no error" cancel context.CancelFunc } // minSamplerSleep is the minimum pause between sampler iterations to prevent // busy-spin when a custom getter returns instantly or the interval is tiny. const minSamplerSleep = 100 * time.Millisecond func (c *CPULoadCriteria) startSampler() { ctx, cancel := context.WithCancel(context.Background()) c.cancel = cancel interval := c.Interval if interval <= 0 { interval = time.Second } // Mark cached as "no sample yet" so callers (e.g. waitForSample in tests) // can detect when the first real sample has been written. c.cached.Store(math.Float64bits(math.NaN())) go func() { // Create a stopped, drained timer. We Reset it after each sample() // so the sleep duration is measured from the right point in time. timer := time.NewTimer(0) if !timer.Stop() { <-timer.C } defer func() { if !timer.Stop() { select { case <-timer.C: default: } } }() for { start := time.Now() c.sample(ctx, interval) // Calculate how long to sleep before the next sample. elapsed := time.Since(start) sleep := interval - elapsed if sleep < minSamplerSleep { sleep = minSamplerSleep } // Safe to Reset: the channel was drained either by the initial // stop+drain above or by the previous <-timer.C in the select. timer.Reset(sleep) select { case <-ctx.Done(): return case <-timer.C: } } }() } // sample performs a single CPU measurement with panic recovery. // If the getter panics, it recovers and fails open (cached → 0) while // recording the underlying error for observability. func (c *CPULoadCriteria) sample(ctx context.Context, interval time.Duration) { defer func() { if r := recover(); r != nil { // Fail open: treat CPU as idle so the middleware never sheds // based on a panicking getter, but record the panic as an error. c.cached.Store(math.Float64bits(0)) c.lastErr.Store(fmt.Errorf("cpu percent getter panicked: %v", r)) } }() percentages, err := c.Getter.PercentWithContext(ctx, interval, false) if err == nil && len(percentages) > 0 { c.cached.Store(math.Float64bits(percentages[0])) } else { // Fail open on sampling errors or empty results: treat CPU as // idle so the middleware never sheds based on stale high values. c.cached.Store(math.Float64bits(0)) // Persist the underlying error so callers can observe persistent // sampling failures, even though the value fails open. if err != nil { c.lastErr.Store(err) } else { c.lastErr.Store(fmt.Errorf("cpu percent getter returned no samples")) } } } // Stop terminates the background CPU sampler goroutine. // It is safe to call Stop concurrently and multiple times (context.CancelFunc // is idempotent). Stop also interrupts any in-progress CPU sampling call, // so shutdown is responsive regardless of the configured Interval. // If called before the sampler has started, no goroutine is ever launched. func (c *CPULoadCriteria) Stop() { c.once.Do(func() { // If Stop is called before Metric/New, set a no-op cancel without // starting the sampler goroutine. sync.Once guarantees that either // this func or startSampler runs, never both. c.cancel = func() {} }) c.cancel() } // Metric returns the most recently sampled CPU usage percentage. // On the first call it starts a background goroutine that continuously // samples CPU usage at the configured Interval, so individual requests // are never blocked waiting for a CPU measurement. // Before the first sample completes, it returns 0 (allowing requests through). // On sampling errors the cached value is reset to 0, preserving fail-open // behaviour: ShouldShed(0) is always false, so requests are allowed through. func (c *CPULoadCriteria) Metric(ctx context.Context) (float64, error) { if err := ctx.Err(); err != nil { // Fail open: return a zero metric so requests are allowed through, // but surface the context error for observability. // Check before starting the sampler so a cancelled context doesn't // needlessly spin up the background goroutine. return 0, err } c.once.Do(c.startSampler) value := math.Float64frombits(c.cached.Load()) if math.IsNaN(value) { // Before the first successful sample, the cached value may be a NaN // sentinel. Expose this as 0 to preserve the documented fail-open // behaviour. value = 0 } return value, nil } func (c *CPULoadCriteria) ShouldShed(metric float64) bool { if metric > c.UpperThreshold*100 { return true } else if metric > c.LowerThreshold*100 { rejectionProbability := (metric - c.LowerThreshold*100) / (c.UpperThreshold - c.LowerThreshold) // #nosec G404 return rand.Float64()*100 < rejectionProbability } return false } type CPUPercentGetter interface { PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) } type DefaultCPUPercentGetter struct{} func (*DefaultCPUPercentGetter) PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) { return cpu.PercentWithContext(ctx, interval, percpu) } ================================================ FILE: v3/loadshed/go.mod ================================================ module github.com/gofiber/contrib/v3/loadshed go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/stretchr/testify v1.11.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/loadshed/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/loadshed/loadshed.go ================================================ package loadshed import ( "time" "github.com/gofiber/fiber/v3" ) type Config struct { // Function to skip this middleware when returned true. Next func(c fiber.Ctx) bool // Criteria defines the criteria to be used for load shedding. Criteria LoadCriteria // OnShed defines a custom handler that will be executed if a request should // be rejected. // // Returning `nil` without writing to the response context allows the // request to proceed to the next handler OnShed func(c fiber.Ctx) error } var ConfigDefault = Config{ Next: nil, Criteria: &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: 10 * time.Second, // Evaluate the average CPU usage over the last 10 seconds. Getter: &DefaultCPUPercentGetter{}, }, } func configWithDefaults(config ...Config) Config { cfg := ConfigDefault if len(config) > 0 { cfg = config[0] } // Determine whether cfg.Criteria is the shared default pointer. // Use type assertion + pointer comparison instead of interface equality (==) // to avoid panics when a custom LoadCriteria holds a non-comparable type. // Also treat a typed-nil *CPULoadCriteria as unset so defaults are applied. typedNilCPU := false isDefault := cfg.Criteria == nil if !isDefault { cfgCPU, cfgOK := cfg.Criteria.(*CPULoadCriteria) if cfgOK && cfgCPU == nil { // Typed-nil *CPULoadCriteria — treat as unset. isDefault = true typedNilCPU = true } else { defCPU, defOK := ConfigDefault.Criteria.(*CPULoadCriteria) isDefault = cfgOK && defOK && cfgCPU == defCPU } } if isDefault { // Clone the default CPULoadCriteria so each middleware instance has // its own sampler state (once/cached/cancel). This covers both the // no-args path (cfg inherits ConfigDefault.Criteria) and the // explicit Config{} path (Criteria is nil). // Use a guarded type assertion: users may replace ConfigDefault.Criteria // with a custom LoadCriteria implementation. if def, ok := ConfigDefault.Criteria.(*CPULoadCriteria); ok { cfg.Criteria = &CPULoadCriteria{ LowerThreshold: def.LowerThreshold, UpperThreshold: def.UpperThreshold, Interval: def.Interval, Getter: def.Getter, } } else if cfg.Criteria == nil || typedNilCPU { // ConfigDefault.Criteria is a custom implementation; use it as-is. cfg.Criteria = ConfigDefault.Criteria } } return cfg } func New(config ...Config) fiber.Handler { cfg := configWithDefaults(config...) return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } // Compute the load metric using the specified criteria metric, err := cfg.Criteria.Metric(c.RequestCtx()) if err != nil { return c.Next() // If unable to get metric, allow the request } // Shed load if the criteria's ShouldShed method returns true if cfg.Criteria.ShouldShed(metric) { // Call the custom OnShed function if cfg.OnShed != nil { return cfg.OnShed(c) } return fiber.NewError(fiber.StatusServiceUnavailable) } return c.Next() } } ================================================ FILE: v3/loadshed/loadshed_test.go ================================================ package loadshed import ( "context" "io" "math" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gofiber/fiber/v3" ) // waitForSample polls the criteria's cached value until the background // sampler has written a real sample (i.e., not NaN). The NaN sentinel is // set by startSampler itself, so this helper never clobbers an // already-populated cache — if the sampler has already completed a sample // before this function is called, it returns immediately. func waitForSample(t *testing.T, criteria *CPULoadCriteria) { t.Helper() // Ensure the sampler is running (triggers lazy start if needed). criteria.once.Do(criteria.startSampler) interval := criteria.Interval if interval <= 0 { interval = time.Second } require.Eventually( t, func() bool { v := math.Float64frombits(criteria.cached.Load()) return !math.IsNaN(v) }, 5*interval, 10*time.Millisecond, "timed out waiting for background sampler to populate cached metric", ) } type MockCPUPercentGetter struct { MockedPercentage []float64 } func (m *MockCPUPercentGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) { return m.MockedPercentage, nil } // PanickingGetter panics on every call (for testing panic recovery). type PanickingGetter struct{} func (*PanickingGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) { panic("boom") } // ErrorGetter always returns an error (for testing fail-open on errors). type ErrorGetter struct{} func (*ErrorGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) { return nil, context.DeadlineExceeded } // EmptyGetter returns an empty slice (for testing fail-open on empty results). type EmptyGetter struct{} func (*EmptyGetter) PercentWithContext(_ context.Context, _ time.Duration, _ bool) ([]float64, error) { return []float64{}, nil } func ReturnOK(c fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } func Test_Loadshed_LowerThreshold(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{89.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) status := resp.StatusCode if status != fiber.StatusOK && status != fiber.StatusServiceUnavailable { t.Fatalf("Expected status code %d or %d but got %d", fiber.StatusOK, fiber.StatusServiceUnavailable, status) } } func Test_Loadshed_DefaultCriteriaWhenNil(t *testing.T) { cfg := configWithDefaults(Config{}) // configWithDefaults should clone the default CPULoadCriteria, not share it. criteria, ok := cfg.Criteria.(*CPULoadCriteria) require.True(t, ok) assert.NotSame(t, ConfigDefault.Criteria, criteria) def := ConfigDefault.Criteria.(*CPULoadCriteria) assert.Equal(t, def.LowerThreshold, criteria.LowerThreshold) assert.Equal(t, def.UpperThreshold, criteria.UpperThreshold) assert.Equal(t, def.Interval, criteria.Interval) } func Test_Loadshed_DefaultCriteriaNoArgs(t *testing.T) { cfg := configWithDefaults() // The no-args path should also clone, not share the default singleton. criteria, ok := cfg.Criteria.(*CPULoadCriteria) require.True(t, ok) assert.NotSame(t, ConfigDefault.Criteria, criteria) def := ConfigDefault.Criteria.(*CPULoadCriteria) assert.Equal(t, def.LowerThreshold, criteria.LowerThreshold) assert.Equal(t, def.UpperThreshold, criteria.UpperThreshold) assert.Equal(t, def.Interval, criteria.Interval) } func Test_Loadshed_MiddleValue(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{93.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) rejectedCount := 0 acceptedCount := 0 iterations := 100000 for i := 0; i < iterations; i++ { resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) if resp.StatusCode == fiber.StatusServiceUnavailable { rejectedCount++ } else { acceptedCount++ } } t.Logf("Accepted: %d, Rejected: %d", acceptedCount, rejectedCount) if acceptedCount == 0 || rejectedCount == 0 { t.Fatalf("Expected both accepted and rejected requests, but got Accepted: %d, Rejected: %d", acceptedCount, rejectedCount) } } func Test_Loadshed_UpperThreshold(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode) } func Test_Loadshed_CustomOnShed(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria cfg.OnShed = func(c fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).Send([]byte{}) } app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode) } func Test_Loadshed_CustomOnShedWithResponse(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria // This OnShed directly sets a response without returning it cfg.OnShed = func(c fiber.Ctx) error { c.Status(fiber.StatusTooManyRequests) return nil } app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusTooManyRequests, resp.StatusCode) } func Test_Loadshed_CustomOnShedWithNilReturn(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria // OnShed returns nil without setting a response cfg.OnShed = func(c fiber.Ctx) error { return nil } app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } func Test_Loadshed_CustomOnShedWithCustomError(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria // OnShed returns a custom error cfg.OnShed = func(c fiber.Ctx) error { return fiber.NewError(fiber.StatusForbidden, "Custom error message") } app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusForbidden, resp.StatusCode) } func Test_Loadshed_CustomOnShedWithResponseAndCustomError(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria // OnShed sets a response and returns a different error // The NewError have higher priority since executed last cfg.OnShed = func(c fiber.Ctx) error { c. Status(fiber.StatusTooManyRequests). SendString("Too many requests") return fiber.NewError( fiber.StatusInternalServerError, "Shed happened", ) } app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) payload, readErr := io.ReadAll(resp.Body) defer resp.Body.Close() assert.Equal(t, string(payload), "Shed happened") assert.Equal(t, nil, err) assert.Equal(t, nil, readErr) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) } func Test_Loadshed_CustomOnShedWithJSON(t *testing.T) { app := fiber.New() mockGetter := &MockCPUPercentGetter{MockedPercentage: []float64{96.0}} criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: mockGetter, } var cfg Config cfg.Criteria = criteria // OnShed returns JSON response cfg.OnShed = func(c fiber.Ctx) error { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "Service is currently unavailable due to high load", "retry_after": 30, }) } app.Use(New(cfg)) app.Get("/", ReturnOK) t.Cleanup(criteria.Stop) waitForSample(t, criteria) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusServiceUnavailable, resp.StatusCode) assert.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type")) } func Test_Loadshed_TypedNilCriteria(t *testing.T) { // A typed-nil *CPULoadCriteria assigned to the Criteria interface should // be treated as unset, and configWithDefaults should clone the default. var nilCriteria *CPULoadCriteria cfg := configWithDefaults(Config{Criteria: nilCriteria}) criteria, ok := cfg.Criteria.(*CPULoadCriteria) require.True(t, ok) assert.NotNil(t, criteria, "typed-nil should be replaced with a cloned default") assert.NotSame(t, ConfigDefault.Criteria, criteria) def := ConfigDefault.Criteria.(*CPULoadCriteria) assert.Equal(t, def.LowerThreshold, criteria.LowerThreshold) assert.Equal(t, def.UpperThreshold, criteria.UpperThreshold) assert.Equal(t, def.Interval, criteria.Interval) } func Test_CPULoadCriteria_StopBeforeStart(t *testing.T) { // Stop() called before the sampler has been started should not panic // and should prevent the sampler from ever launching. criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: &MockCPUPercentGetter{MockedPercentage: []float64{50.0}}, } // Should not panic. criteria.Stop() // Metric should still work (returns 0, nil) without starting a sampler. metric, err := criteria.Metric(context.Background()) assert.NoError(t, err) assert.Equal(t, float64(0), metric) } func Test_CPULoadCriteria_PanickingGetter(t *testing.T) { // A getter that panics should not crash the process; the sampler // should recover and fail open (cached → 0). criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: 100 * time.Millisecond, Getter: &PanickingGetter{}, } t.Cleanup(criteria.Stop) // Start the sampler, then use NaN sentinel to detect when it has run. criteria.once.Do(criteria.startSampler) waitForSample(t, criteria) // The sampler should still be alive and returning 0 (fail-open). metric, err := criteria.Metric(context.Background()) assert.NoError(t, err) assert.Equal(t, float64(0), metric) } func Test_CPULoadCriteria_ErrorGetter(t *testing.T) { // A getter that returns errors should fail open (cached → 0). criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: 100 * time.Millisecond, Getter: &ErrorGetter{}, } t.Cleanup(criteria.Stop) criteria.once.Do(criteria.startSampler) waitForSample(t, criteria) metric, err := criteria.Metric(context.Background()) assert.NoError(t, err) assert.Equal(t, float64(0), metric) } func Test_CPULoadCriteria_EmptyGetter(t *testing.T) { // A getter that returns an empty slice should fail open (cached → 0). criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: 100 * time.Millisecond, Getter: &EmptyGetter{}, } t.Cleanup(criteria.Stop) criteria.once.Do(criteria.startSampler) waitForSample(t, criteria) metric, err := criteria.Metric(context.Background()) assert.NoError(t, err) assert.Equal(t, float64(0), metric) } func Test_CPULoadCriteria_MetricCancelledContext(t *testing.T) { // Metric() with a cancelled context should fail open (return 0) // but surface the context error for observability. criteria := &CPULoadCriteria{ LowerThreshold: 0.90, UpperThreshold: 0.95, Interval: time.Second, Getter: &MockCPUPercentGetter{MockedPercentage: []float64{50.0}}, } t.Cleanup(criteria.Stop) ctx, cancel := context.WithCancel(context.Background()) cancel() metric, err := criteria.Metric(ctx) assert.ErrorIs(t, err, context.Canceled) assert.Equal(t, float64(0), metric) } ================================================ FILE: v3/monitor/README.md ================================================ --- id: monitor --- # Monitor ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*monitor*) ![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7) ![Test](https://github.com/gofiber/contrib/workflows/Test%20Monitor/badge.svg) Monitor middleware for [Fiber](https://github.com/gofiber/fiber) that reports server metrics, inspired by [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor) ![](https://i.imgur.com/nHAtBpJ.gif) **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/monitor ``` ### Signature ```go monitor.New(config ...monitor.Config) fiber.Handler ``` ### Config | Property | Type | Description | Default | | :--------- | :------------------------ | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- | | Title | `string` | Metrics page title. | `Fiber Monitor` | | Refresh | `time.Duration` | Refresh period. | `3 seconds` | | APIOnly | `bool` | Whether the service should expose only the montioring API. | `false` | | Next | `func(c fiber.Ctx) bool` | Define a function to add custom fields. | `nil` | | CustomHead | `string` | Custom HTML code to Head Section(Before End). | `empty` | | 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` | | 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` | ### Example ```go package main import ( "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/monitor" ) func main() { app := fiber.New() // Initialize default config (Assign the middleware to /metrics) app.Get("/metrics", monitor.New()) // Or extend your config for customization // Assign the middleware to /metrics // and change the Title to `MyService Metrics Page` app.Get("/metrics", monitor.New(monitor.Config{Title: "MyService Metrics Page"})) log.Fatal(app.Listen(":3000")) } ``` ## Default Config ```go var ConfigDefault = Config{ Title: defaultTitle, Refresh: defaultRefresh, FontURL: defaultFontURL, ChartJsURL: defaultChartJSURL, CustomHead: defaultCustomHead, APIOnly: false, Next: nil, } ``` ================================================ FILE: v3/monitor/config.go ================================================ package monitor import ( "time" "github.com/gofiber/fiber/v3" ) // Config defines the config for middleware. type Config struct { // Metrics page title // // Optional. Default: "Fiber Monitor" Title string // Refresh period // // Optional. Default: 3 seconds Refresh time.Duration // Whether the service should expose only the monitoring API. // // Optional. Default: false APIOnly bool // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool // Custom HTML Code to Head Section(Before End) // // Optional. Default: empty CustomHead string // FontURL to specify font resource path or URL. You can also use a relative path. // // Optional. Default: https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap FontURL string // ChartJSURL to specify ChartJS library path or URL. You can also use a relative path. // // Optional. Default: https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js ChartJSURL string index string } var ConfigDefault = Config{ Title: defaultTitle, Refresh: defaultRefresh, FontURL: defaultFontURL, ChartJSURL: defaultChartJSURL, CustomHead: defaultCustomHead, APIOnly: false, Next: nil, index: newIndex(viewBag{ defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead, }), } func configDefault(config ...Config) Config { // Users can change ConfigDefault.Title/Refresh which then // become incompatible with ConfigDefault.index if ConfigDefault.Title != defaultTitle || ConfigDefault.Refresh != defaultRefresh || ConfigDefault.FontURL != defaultFontURL || ConfigDefault.ChartJSURL != defaultChartJSURL || ConfigDefault.CustomHead != defaultCustomHead { if ConfigDefault.Refresh < minRefresh { ConfigDefault.Refresh = minRefresh } // update default index with new default title/refresh ConfigDefault.index = newIndex(viewBag{ ConfigDefault.Title, ConfigDefault.Refresh, ConfigDefault.FontURL, ConfigDefault.ChartJSURL, ConfigDefault.CustomHead, }) } // Return default config if nothing provided if len(config) < 1 { return ConfigDefault } // Override default config cfg := config[0] // Set default values if cfg.Title == "" { cfg.Title = ConfigDefault.Title } if cfg.Refresh == 0 { cfg.Refresh = ConfigDefault.Refresh } if cfg.FontURL == "" { cfg.FontURL = defaultFontURL } if cfg.ChartJSURL == "" { cfg.ChartJSURL = defaultChartJSURL } if cfg.Refresh < minRefresh { cfg.Refresh = minRefresh } if cfg.Next == nil { cfg.Next = ConfigDefault.Next } if !cfg.APIOnly { cfg.APIOnly = ConfigDefault.APIOnly } // update cfg.index with custom title/refresh cfg.index = newIndex(viewBag{ title: cfg.Title, refresh: cfg.Refresh, fontURL: cfg.FontURL, chartJSURL: cfg.ChartJSURL, customHead: cfg.CustomHead, }) return cfg } ================================================ FILE: v3/monitor/config_test.go ================================================ package monitor import ( "testing" "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" ) func Test_Config_Default(t *testing.T) { t.Parallel() t.Run("use default", func(t *testing.T) { t.Parallel() cfg := configDefault() assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) t.Run("set title", func(t *testing.T) { t.Parallel() title := "title" cfg := configDefault(Config{ Title: title, }) assert.Equal(t, title, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{title, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) t.Run("set refresh less than default", func(t *testing.T) { t.Parallel() cfg := configDefault(Config{ Refresh: 100 * time.Millisecond, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, minRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, minRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) t.Run("set refresh", func(t *testing.T) { t.Parallel() refresh := time.Second cfg := configDefault(Config{ Refresh: refresh, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, refresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, refresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) t.Run("set font url", func(t *testing.T) { t.Parallel() fontURL := "https://example.com" cfg := configDefault(Config{ FontURL: fontURL, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, fontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, fontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) t.Run("set chart js url", func(t *testing.T) { t.Parallel() chartURL := "http://example.com" cfg := configDefault(Config{ ChartJSURL: chartURL, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, chartURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, chartURL, defaultCustomHead}), cfg.index) }) t.Run("set custom head", func(t *testing.T) { t.Parallel() head := "head" cfg := configDefault(Config{ CustomHead: head, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, head, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, head}), cfg.index) }) t.Run("set api only", func(t *testing.T) { t.Parallel() cfg := configDefault(Config{ APIOnly: true, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, true, cfg.APIOnly) assert.IsType(t, (func(fiber.Ctx) bool)(nil), cfg.Next) assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) t.Run("set next", func(t *testing.T) { t.Parallel() f := func(c fiber.Ctx) bool { return true } cfg := configDefault(Config{ Next: f, }) assert.Equal(t, defaultTitle, cfg.Title) assert.Equal(t, defaultRefresh, cfg.Refresh) assert.Equal(t, defaultFontURL, cfg.FontURL) assert.Equal(t, defaultChartJSURL, cfg.ChartJSURL) assert.Equal(t, defaultCustomHead, cfg.CustomHead) assert.Equal(t, false, cfg.APIOnly) assert.Equal(t, f(nil), cfg.Next(nil)) assert.Equal(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) }) } ================================================ FILE: v3/monitor/go.mod ================================================ module github.com/gofiber/contrib/v3/monitor go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/shirou/gopsutil/v4 v4.26.3 github.com/stretchr/testify v1.11.1 github.com/valyala/fasthttp v1.70.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/monitor/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/monitor/index.go ================================================ package monitor import ( "strings" "time" "github.com/gofiber/utils/v2" ) type viewBag struct { title string refresh time.Duration fontURL string chartJSURL string customHead string } // returns index with new title/refresh func newIndex(dat viewBag) string { timeout := dat.refresh.Milliseconds() - timeoutDiff if timeout < timeoutDiff { timeout = timeoutDiff } ts := utils.FormatInt(timeout) replacer := strings.NewReplacer("$TITLE", dat.title, "$TIMEOUT", ts, "$FONT_URL", dat.fontURL, "$CHART_JS_URL", dat.chartJSURL, "$CUSTOM_HEAD", dat.customHead, ) return replacer.Replace(indexHTML) } const ( defaultTitle = "Fiber Monitor" defaultRefresh = 3 * time.Second timeoutDiff = 200 // timeout will be Refresh (in milliseconds) - timeoutDiff minRefresh = timeoutDiff * time.Millisecond defaultFontURL = `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap` defaultChartJSURL = `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js` defaultCustomHead = `` // parametrized by $TITLE and $TIMEOUT indexHTML = ` $TITLE

$TITLE

CPU Usage

0.00%

Memory Usage

0.00 MB

Response Time

0ms

Open Connections

0

` ) ================================================ FILE: v3/monitor/monitor.go ================================================ package monitor import ( "os" "runtime" "sync" "sync/atomic" "time" "github.com/gofiber/fiber/v3" "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/load" "github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/net" "github.com/shirou/gopsutil/v4/process" ) type stats struct { PID statsPID `json:"pid"` OS statsOS `json:"os"` } type statsPID struct { CPU float64 `json:"cpu"` RAM uint64 `json:"ram"` Conns int `json:"conns"` } type statsOS struct { CPU float64 `json:"cpu"` RAM uint64 `json:"ram"` TotalRAM uint64 `json:"total_ram"` LoadAvg float64 `json:"load_avg"` Conns int `json:"conns"` } var ( monitPIDCPU atomic.Value monitPIDRAM atomic.Value monitPIDConns atomic.Value monitOSCPU atomic.Value monitOSRAM atomic.Value monitOSTotalRAM atomic.Value monitOSLoadAvg atomic.Value monitOSConns atomic.Value ) var ( mutex sync.RWMutex once sync.Once data = &stats{} ) // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) // Start routine to update statistics once.Do(func() { p, _ := process.NewProcess(int32(os.Getpid())) //nolint:errcheck // TODO: Handle error numcpu := runtime.NumCPU() updateStatistics(p, numcpu) go func() { for { time.Sleep(cfg.Refresh) updateStatistics(p, numcpu) } }() }) // Return new handler //nolint:errcheck // Ignore the type-assertion errors return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } if c.Method() != fiber.MethodGet { return fiber.ErrMethodNotAllowed } if c.Get(fiber.HeaderAccept) == fiber.MIMEApplicationJSON || cfg.APIOnly { mutex.Lock() data.PID.CPU, _ = monitPIDCPU.Load().(float64) data.PID.RAM, _ = monitPIDRAM.Load().(uint64) data.PID.Conns, _ = monitPIDConns.Load().(int) data.OS.CPU, _ = monitOSCPU.Load().(float64) data.OS.RAM, _ = monitOSRAM.Load().(uint64) data.OS.TotalRAM, _ = monitOSTotalRAM.Load().(uint64) data.OS.LoadAvg, _ = monitOSLoadAvg.Load().(float64) data.OS.Conns, _ = monitOSConns.Load().(int) mutex.Unlock() return c.Status(fiber.StatusOK).JSON(data) } c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) return c.Status(fiber.StatusOK).SendString(cfg.index) } } func updateStatistics(p *process.Process, numcpu int) { pidCPU, err := p.Percent(0) if err == nil { monitPIDCPU.Store(pidCPU / float64(numcpu)) } if osCPU, err := cpu.Percent(0, false); err == nil && len(osCPU) > 0 { monitOSCPU.Store(osCPU[0]) } if pidRAM, err := p.MemoryInfo(); err == nil && pidRAM != nil { monitPIDRAM.Store(pidRAM.RSS) } if osRAM, err := mem.VirtualMemory(); err == nil && osRAM != nil { monitOSRAM.Store(osRAM.Used) monitOSTotalRAM.Store(osRAM.Total) } if loadAvg, err := load.Avg(); err == nil && loadAvg != nil { monitOSLoadAvg.Store(loadAvg.Load1) } pidConns, err := net.ConnectionsPid("tcp", p.Pid) if err == nil { monitPIDConns.Store(len(pidConns)) } osConns, err := net.Connections("tcp") if err == nil { monitOSConns.Store(len(osConns)) } } ================================================ FILE: v3/monitor/monitor_test.go ================================================ package monitor import ( "bytes" "fmt" "io" "net/http/httptest" "testing" "time" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" "github.com/valyala/fasthttp" ) func Test_Monitor_405(t *testing.T) { t.Parallel() app := fiber.New() app.Use("/", New()) resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 405, resp.StatusCode) } func Test_Monitor_Html(t *testing.T) { t.Parallel() app := fiber.New() // defaults app.Get("/", New()) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) buf, err := io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, true, bytes.Contains(buf, []byte(""+defaultTitle+""))) timeoutLine := fmt.Sprintf("setTimeout(fetchJSON, %d)", defaultRefresh.Milliseconds()-timeoutDiff) assert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine))) // custom config conf := Config{Title: "New " + defaultTitle, Refresh: defaultRefresh + time.Second} app.Get("/custom", New(conf)) resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/custom", nil)) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) buf, err = io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, true, bytes.Contains(buf, []byte(""+conf.Title+""))) timeoutLine = fmt.Sprintf("setTimeout(fetchJSON, %d)", conf.Refresh.Milliseconds()-timeoutDiff) assert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine))) } func Test_Monitor_Html_CustomCodes(t *testing.T) { t.Parallel() app := fiber.New() // defaults app.Get("/", New()) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) buf, err := io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, true, bytes.Contains(buf, []byte(""+defaultTitle+""))) timeoutLine := fmt.Sprintf("setTimeout(fetchJSON, %d)", defaultRefresh.Milliseconds()-timeoutDiff) assert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine))) // custom config conf := Config{ Title: "New " + defaultTitle, Refresh: defaultRefresh + time.Second, ChartJSURL: "https://cdnjs.com/libraries/Chart.js", FontURL: "/public/my-font.css", CustomHead: ``, } app.Get("/custom", New(conf)) resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/custom", nil)) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) buf, err = io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, true, bytes.Contains(buf, []byte(""+conf.Title+""))) assert.Equal(t, true, bytes.Contains(buf, []byte("https://cdnjs.com/libraries/Chart.js"))) assert.Equal(t, true, bytes.Contains(buf, []byte("/public/my-font.css"))) assert.Equal(t, true, bytes.Contains(buf, []byte(conf.CustomHead))) timeoutLine = fmt.Sprintf("setTimeout(fetchJSON, %d)", conf.Refresh.Milliseconds()-timeoutDiff) assert.Equal(t, true, bytes.Contains(buf, []byte(timeoutLine))) } // go test -run Test_Monitor_JSON -race func Test_Monitor_JSON(t *testing.T) { t.Parallel() app := fiber.New() app.Get("/", New()) req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON) resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) b, err := io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, true, bytes.Contains(b, []byte("pid"))) assert.Equal(t, true, bytes.Contains(b, []byte("os"))) } // go test -v -run=^$ -bench=Benchmark_Monitor -benchmem -count=4 func Benchmark_Monitor(b *testing.B) { app := fiber.New() app.Get("/", New()) h := app.Handler() fctx := &fasthttp.RequestCtx{} fctx.Request.Header.SetMethod(fiber.MethodGet) fctx.Request.SetRequestURI("/") fctx.Request.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON) b.ReportAllocs() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { h(fctx) } }) assert.Equal(b, 200, fctx.Response.Header.StatusCode()) assert.Equal(b, fiber.MIMEApplicationJSONCharsetUTF8, string(fctx.Response.Header.Peek(fiber.HeaderContentType))) } // go test -run Test_Monitor_Next func Test_Monitor_Next(t *testing.T) { t.Parallel() app := fiber.New() app.Use("/", New(Config{ Next: func(_ fiber.Ctx) bool { return true }, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, 404, resp.StatusCode) } // go test -run Test_Monitor_APIOnly -race func Test_Monitor_APIOnly(t *testing.T) { app := fiber.New() app.Get("/", New(Config{ APIOnly: true, })) req := httptest.NewRequest(fiber.MethodGet, "/", nil) req.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON) resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) b, err := io.ReadAll(resp.Body) assert.Equal(t, nil, err) assert.Equal(t, true, bytes.Contains(b, []byte("pid"))) assert.Equal(t, true, bytes.Contains(b, []byte("os"))) } ================================================ FILE: v3/newrelic/README.md ================================================ --- id: newrelic --- # New Relic ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*newrelic*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20newrelic/badge.svg) [New Relic](https://github.com/newrelic/go-agent) support for Fiber. Incoming 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. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/newrelic ``` ## Signature ```go middleware.New(config middleware.Config) fiber.Handler middleware.FromContext(ctx any) *nr.Transaction // nr "github.com/newrelic/go-agent/v3/newrelic" ``` `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`). ## Config | Property | Type | Description | Default | |:-----------------------|:-----------------|:------------------------------------------------------------|:--------------------------------| | License | `string` | Required - New Relic License Key | `""` | | AppName | `string` | New Relic Application Name | `fiber-api` | | Enabled | `bool` | Enable/Disable New Relic | `false` | | ~~TransportType~~ | ~~`string`~~ | ~~Can be HTTP or HTTPS~~ (Deprecated) | ~~`"HTTP"`~~ | | Application | `Application` | Existing New Relic App | `nil` | | ErrorStatusCodeHandler | `func(c fiber.Ctx, err error) int` | If you want to change newrelic status code, you can use it. | `DefaultErrorStatusCodeHandler` | | Next | `func(c fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | | RequestHeaderFilter | `func(key, value string) bool` | Return `true` to forward a request header to New Relic, `false` to skip it. | `nil` (forward all headers) | ## Usage ```go package main import ( "github.com/gofiber/fiber/v3" middleware "github.com/gofiber/contrib/v3/newrelic" ) func main() { app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) cfg := middleware.Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "MyCustomApi", Enabled: true, } app.Use(middleware.New(cfg)) app.Listen(":8080") } ``` ## Usage with existing New Relic application ```go package main import ( "github.com/gofiber/fiber/v3" middleware "github.com/gofiber/contrib/v3/newrelic" nr "github.com/newrelic/go-agent/v3/newrelic" ) func main() { nrApp, err := nr.NewApplication( nr.ConfigAppName("MyCustomApi"), nr.ConfigLicense("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), nr.ConfigEnabled(true), ) app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) app.Get("/foo", func(ctx fiber.Ctx) error { txn := middleware.FromContext(ctx) segment := txn.StartSegment("foo segment") defer segment.End() // do foo return nil }) cfg := middleware.Config{ Application: nrApp, } app.Use(middleware.New(cfg)) app.Listen(":8080") } ``` ## Retrieving the transaction with PassLocalsToContext When `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: ```go // From a fiber.Ctx (most common usage) txn := middleware.FromContext(c) // From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled) txn := middleware.FromContext(c.Context()) ``` ================================================ FILE: v3/newrelic/fiber.go ================================================ package newrelic import ( "context" "fmt" "net/http" "net/url" "strings" "github.com/gofiber/utils/v2" "github.com/gofiber/fiber/v3" "github.com/newrelic/go-agent/v3/newrelic" ) // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. type contextKey int const ( transactionKey contextKey = iota ) type Config struct { // License parameter is required to initialize newrelic application License string // AppName parameter passed to set app name, default is fiber-api AppName string // Enabled parameter passed to enable/disable newrelic Enabled bool // TransportType can be HTTP or HTTPS, default is HTTP // Deprecated: The Transport type now acquiring from request URL scheme internally TransportType string // Application field is required to use an existing newrelic application Application *newrelic.Application // ErrorStatusCodeHandler is executed when an error is returned from handler // Optional. Default: DefaultErrorStatusCodeHandler ErrorStatusCodeHandler func(c fiber.Ctx, err error) int // Next defines a function to skip this middleware when returned true. // Optional. Default: nil Next func(c fiber.Ctx) bool // RequestHeaderFilter controls which inbound request headers are forwarded to // New Relic via WebRequest.Header. // Return true to include a header, false to exclude it. // Optional. Default: include all headers. RequestHeaderFilter func(key, value string) bool } var ConfigDefault = Config{ Application: nil, License: "", AppName: "fiber-api", Enabled: false, ErrorStatusCodeHandler: DefaultErrorStatusCodeHandler, Next: nil, RequestHeaderFilter: nil, } func New(cfg Config) fiber.Handler { var app *newrelic.Application var err error if cfg.ErrorStatusCodeHandler == nil { cfg.ErrorStatusCodeHandler = ConfigDefault.ErrorStatusCodeHandler } if cfg.Application != nil { app = cfg.Application } else { if cfg.AppName == "" { cfg.AppName = ConfigDefault.AppName } if cfg.License == "" { panic(fmt.Errorf("unable to create New Relic Application -> License can not be empty")) } app, err = newrelic.NewApplication( newrelic.ConfigAppName(cfg.AppName), newrelic.ConfigLicense(cfg.License), newrelic.ConfigEnabled(cfg.Enabled), ) if err != nil { panic(fmt.Errorf("unable to create New Relic Application -> %w", err)) } } return func(c fiber.Ctx) error { if cfg.Next != nil && cfg.Next(c) { return c.Next() } txn := app.StartTransaction(createTransactionName(c)) defer txn.End() var ( host = utils.CopyString(c.Hostname()) method = utils.CopyString(c.Method()) ) scheme := c.Request().URI().Scheme() txn.SetWebRequest(createWebRequest(c, host, method, string(scheme), cfg.RequestHeaderFilter)) fiber.StoreInContext(c, transactionKey, txn) c.SetContext(newrelic.NewContext(c.Context(), txn)) handlerErr := c.Next() statusCode := c.RequestCtx().Response.StatusCode() if handlerErr != nil { statusCode = cfg.ErrorStatusCodeHandler(c, handlerErr) txn.NoticeError(handlerErr) } txn.SetWebResponse(nil).WriteHeader(statusCode) return handlerErr } } // FromContext returns the Transaction from the context if present, and nil otherwise. // It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context. func FromContext(ctx any) *newrelic.Transaction { if txn, ok := fiber.ValueFromContext[*newrelic.Transaction](ctx, transactionKey); ok { return txn } if ctx, ok := ctx.(context.Context); ok { return newrelic.FromContext(ctx) } return nil } func createTransactionName(c fiber.Ctx) string { return fmt.Sprintf("%s %s", c.Request().Header.Method(), c.Request().URI().Path()) } func createWebRequest(c fiber.Ctx, host, method, scheme string, filter func(key, value string) bool) newrelic.WebRequest { headers := make(http.Header, c.Request().Header.Len()) for key, value := range c.Request().Header.All() { headerKey := string(key) headerValue := string(value) if filter != nil && !filter(headerKey, headerValue) { continue } headers.Add(headerKey, headerValue) } return newrelic.WebRequest{ Header: headers, Host: host, Method: method, Transport: transport(scheme), URL: &url.URL{ Host: host, Scheme: scheme, Path: string(c.Request().URI().Path()), RawQuery: string(c.Request().URI().QueryString()), }, } } func transport(schema string) newrelic.TransportType { if strings.HasPrefix(schema, "https") { return newrelic.TransportHTTPS } if strings.HasPrefix(schema, "http") { return newrelic.TransportHTTP } return newrelic.TransportUnknown } func DefaultErrorStatusCodeHandler(c fiber.Ctx, err error) int { if fiberErr, ok := err.(*fiber.Error); ok { return fiberErr.Code } return c.RequestCtx().Response.StatusCode() } ================================================ FILE: v3/newrelic/fiber_test.go ================================================ package newrelic import ( "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v3" "github.com/newrelic/go-agent/v3/newrelic" "github.com/stretchr/testify/assert" ) func TestNewRelicAppConfig(t *testing.T) { t.Run("Panic occurs when License empty", func(t *testing.T) { assert.Panics(t, func() { New(Config{ License: "", AppName: "", Enabled: false, }) }) }) t.Run("Run without panic when License not empty", func(t *testing.T) { assert.NotPanics(t, func() { New(Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "", Enabled: false, }) }) }) t.Run("Panic when License is invalid length", func(t *testing.T) { assert.Panics(t, func() { New(Config{ License: "invalid_key", AppName: "", Enabled: false, }) }) }) t.Run("Run successfully as middleware", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "", Enabled: true, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest(http.MethodGet, "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, http.StatusOK, resp.StatusCode) }) t.Run("Run successfully as middleware", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "", Enabled: true, } newRelicApp, _ := newrelic.NewApplication( newrelic.ConfigAppName(cfg.AppName), newrelic.ConfigLicense(cfg.License), newrelic.ConfigEnabled(cfg.Enabled), ) cfg.Application = newRelicApp app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) t.Run("Test for invalid URL", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/invalid-url", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 404, resp.StatusCode) }) t.Run("Test HTTP transport type", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) t.Run("Test http transport type (lowercase)", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) t.Run("Test HTTPS transport type", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) t.Run("Test using existing newrelic application (configured)", func(t *testing.T) { app := fiber.New() newrelicApp, err := newrelic.NewApplication( newrelic.ConfigAppName("testApp"), newrelic.ConfigLicense("0123456789abcdef0123456789abcdef01234567"), newrelic.ConfigEnabled(true), ) cfg := Config{ Application: newrelicApp, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) assert.NoError(t, err) assert.NotNil(t, newrelicApp) r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) t.Run("Assert panic with existing newrelic application (no config)", func(t *testing.T) { assert.Panics(t, func() { app := fiber.New() newrelicApp, err := newrelic.NewApplication() cfg := Config{ Application: newrelicApp, } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) assert.Error(t, err) assert.Nil(t, newrelicApp) r := httptest.NewRequest("GET", "/", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) }) t.Run("config should use default error status code handler", func(t *testing.T) { // given app := fiber.New() app.Use(New(Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, })) app.Get("/", func(ctx fiber.Ctx) error { return errors.New("system error") }) // when r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) }) t.Run("config should use custom error status code handler when error status code handler is provided", func(t *testing.T) { // given var ( app = fiber.New() errorStatusCodeHandlerCalled = false ) errorStatusCodeHandler := func(c fiber.Ctx, err error) int { errorStatusCodeHandlerCalled = true return http.StatusInternalServerError } app.Use(New(Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, ErrorStatusCodeHandler: errorStatusCodeHandler, })) app.Get("/", func(ctx fiber.Ctx) error { return errors.New("system error") }) // when r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) assert.True(t, errorStatusCodeHandlerCalled) }) t.Run("Skip New Relic execution if next function is set", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "", Enabled: true, Next: func(c fiber.Ctx) bool { return c.OriginalURL() == "/jump" }, } newRelicApp, _ := newrelic.NewApplication( newrelic.ConfigAppName(cfg.AppName), newrelic.ConfigLicense(cfg.License), newrelic.ConfigEnabled(cfg.Enabled), ) cfg.Application = newRelicApp app.Use(New(cfg)) app.Get("/jump", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/jump", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) t.Run("Continue New Relic execution if next function is set", func(t *testing.T) { app := fiber.New() cfg := Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "", Enabled: true, Next: func(c fiber.Ctx) bool { return c.OriginalURL() == "/jump" }, } newRelicApp, _ := newrelic.NewApplication( newrelic.ConfigAppName(cfg.AppName), newrelic.ConfigLicense(cfg.License), newrelic.ConfigEnabled(cfg.Enabled), ) cfg.Application = newRelicApp app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 200, resp.StatusCode) }) } func TestDefaultErrorStatusCodeHandler(t *testing.T) { t.Run("should return fiber status code when error is fiber error", func(t *testing.T) { // given err := &fiber.Error{ Code: http.StatusNotFound, } // when statusCode := DefaultErrorStatusCodeHandler(nil, err) // then assert.Equal(t, http.StatusNotFound, statusCode) }) t.Run("should return context status code when error is not fiber error", func(t *testing.T) { // given app := fiber.New() app.Use(New(Config{ License: "0123456789abcdef0123456789abcdef01234567", AppName: "", Enabled: true, })) app.Get("/", func(ctx fiber.Ctx) error { err := ctx.SendStatus(http.StatusNotFound) assert.Equal(t, http.StatusNotFound, DefaultErrorStatusCodeHandler(ctx, err)) return err }) // when r := httptest.NewRequest("GET", "/", nil) r.Host = "localhost" resp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.NoError(t, err) assert.Equal(t, http.StatusNotFound, resp.StatusCode) }) } func TestFromContext(t *testing.T) { // given cfg := Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", AppName: "", Enabled: true, } app := fiber.New() app.Use(New(cfg)) app.Get("/foo", func(ctx fiber.Ctx) error { tx := FromContext(ctx) assert.NotNil(t, tx) if tx != nil { segment := tx.StartSegment("foo") defer segment.End() } return ctx.SendStatus(http.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "/foo", http.NoBody) req.Host = "localhost" // when res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) // then assert.Nil(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) } func TestCreateWebRequest(t *testing.T) { t.Run("should include inbound headers for distributed tracing", func(t *testing.T) { app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { req := createWebRequest(ctx, ctx.Hostname(), ctx.Method(), string(ctx.Request().URI().Scheme()), nil) assert.Equal(t, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", req.Header.Get("traceparent")) assert.ElementsMatch(t, []string{"abc", "def"}, req.Header.Values("X-Custom")) return ctx.SendStatus(http.StatusNoContent) }) r := httptest.NewRequest(http.MethodGet, "/", nil) r.Host = "example.com" r.Header.Set("traceparent", "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") r.Header.Add("X-Custom", "abc") r.Header.Add("X-Custom", "def") resp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.NoError(t, err) assert.Equal(t, http.StatusNoContent, resp.StatusCode) }) t.Run("should apply request header filter when configured", func(t *testing.T) { app := fiber.New() app.Get("/", func(ctx fiber.Ctx) error { req := createWebRequest(ctx, ctx.Hostname(), ctx.Method(), string(ctx.Request().URI().Scheme()), func(key, _ string) bool { return strings.EqualFold(key, "traceparent") }) assert.Equal(t, "trace-value", req.Header.Get("traceparent")) assert.Empty(t, req.Header.Values("Authorization")) return ctx.SendStatus(http.StatusNoContent) }) r := httptest.NewRequest(http.MethodGet, "/", nil) r.Host = "example.com" r.Header.Set("traceparent", "trace-value") r.Header.Set("Authorization", "Bearer secret") resp, err := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.NoError(t, err) assert.Equal(t, http.StatusNoContent, resp.StatusCode) }) } func TestFromContext_PassLocalsToContext(t *testing.T) { cfg := Config{ License: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", Enabled: true, } app := fiber.New(fiber.Config{PassLocalsToContext: true}) app.Use(New(cfg)) app.Get("/foo", func(ctx fiber.Ctx) error { tx := FromContext(ctx) txFromContext := FromContext(ctx.Context()) assert.NotNil(t, tx) assert.NotNil(t, txFromContext) return ctx.SendStatus(http.StatusOK) }) req := httptest.NewRequest(http.MethodGet, "/foo", http.NoBody) req.Host = "localhost" res, err := app.Test(req, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) } ================================================ FILE: v3/newrelic/go.mod ================================================ module github.com/gofiber/contrib/v3/newrelic go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/newrelic/go-agent/v3 v3.43.2 github.com/stretchr/testify v1.11.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/newrelic/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/newrelic/go-agent/v3 v3.43.2 h1:I8M0Do/sPtbT0daCMrxc9G6GeST+eDgsIpRHFAFRdOg= github.com/newrelic/go-agent/v3 v3.43.2/go.mod h1:MFXnCId5xXMIJI6A/kbkg0DO48EVTsKcmNijMYphzTg= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/opa/README.md ================================================ --- id: opa --- # OPA ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*opa*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20opa/badge.svg) [Open Policy Agent](https://github.com/open-policy-agent/opa) support for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/opa ``` ## Signature ```go opa.New(config opa.Config) fiber.Handler ``` ## Config | Property | Type | Description | Default | |:----------------------|:--------------------|:-------------------------------------------------------------|:--------------------------------------------------------------------| | RegoQuery | `string` | Required - Rego query | - | | RegoPolicy | `io.Reader` | Required - Rego policy | - | | IncludeQueryString | `bool` | Include query string as input to rego policy | `false` | | DeniedStatusCode | `int` | Http status code to return when policy denies request | `400` | | DeniedResponseMessage | `string` | Http response body text to return when policy denies request | `""` | | IncludeHeaders | `[]string` | Include headers as input to rego policy | - | | InputCreationMethod | `InputCreationFunc` | Use your own function to provide input for OPA | `func defaultInput(ctx fiber.Ctx) (map[string]interface{}, error)` | ## Types ```go type InputCreationFunc func(c fiber.Ctx) (map[string]interface{}, error) ``` ## Usage OPA Fiber middleware sends the following example data to the policy engine as input: ```json { "method": "GET", "path": "/somePath", "query": { "name": ["John Doe"] }, "headers": { "Accept": "application/json", "Content-Type": "application/json" } } ``` ```go package main import ( "bytes" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/opa" ) func main() { app := fiber.New() module := ` package example.authz default allow := false allow if { input.method == "GET" } ` cfg := opa.Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), IncludeQueryString: true, DeniedStatusCode: fiber.StatusForbidden, DeniedResponseMessage: "status forbidden", IncludeHeaders: []string{"Authorization"}, InputCreationMethod: func(ctx fiber.Ctx) (map[string]interface{}, error) { return map[string]interface{}{ "method": ctx.Method(), "path": ctx.Path(), }, nil }, } app.Use(opa.New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) app.Listen(":8080") } ``` ================================================ FILE: v3/opa/fiber.go ================================================ package opa import ( "context" "fmt" "io" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" "github.com/open-policy-agent/opa/v1/rego" ) type InputCreationFunc func(c fiber.Ctx) (map[string]interface{}, error) type Config struct { RegoPolicy io.Reader RegoQuery string IncludeHeaders []string IncludeQueryString bool DeniedStatusCode int DeniedResponseMessage string InputCreationMethod InputCreationFunc } func New(cfg Config) fiber.Handler { err := cfg.fillAndValidate() if err != nil { panic(err) } readedBytes, err := io.ReadAll(cfg.RegoPolicy) if err != nil { panic(fmt.Sprint("could not read rego policy %w", err)) } query, err := rego.New( rego.Query(cfg.RegoQuery), rego.Module("policy.rego", utils.UnsafeString(readedBytes)), ).PrepareForEval(context.Background()) if err != nil { panic(fmt.Sprint("rego policy error: %w", err)) } return func(c fiber.Ctx) error { input, err := cfg.InputCreationMethod(c) if err != nil { c.Response().SetStatusCode(fiber.StatusInternalServerError) c.Response().SetBodyString(fmt.Sprintf("Error creating input: %s", err)) return err } if cfg.IncludeQueryString { queryStringData := make(map[string][]string) for key, value := range c.Request().URI().QueryArgs().All() { k := utils.UnsafeString(key) queryStringData[k] = append(queryStringData[k], utils.UnsafeString(value)) } input["query"] = queryStringData } if len(cfg.IncludeHeaders) > 0 { headers := make(map[string]string) for _, header := range cfg.IncludeHeaders { headers[header] = c.Get(header) } input["headers"] = headers } res, err := query.Eval(context.Background(), rego.EvalInput(input)) if err != nil { c.Response().SetStatusCode(fiber.StatusInternalServerError) c.Response().SetBodyString(fmt.Sprintf("Error evaluating rego policy: %s", err)) return err } if !res.Allowed() { c.Response().SetStatusCode(cfg.DeniedStatusCode) c.Response().SetBodyString(cfg.DeniedResponseMessage) return nil } return c.Next() } } func (c *Config) fillAndValidate() error { if c.RegoQuery == "" { return fmt.Errorf("rego query can not be empty") } if c.DeniedStatusCode == 0 { c.DeniedStatusCode = fiber.StatusBadRequest } if c.DeniedResponseMessage == "" { c.DeniedResponseMessage = fiber.ErrBadRequest.Error() } if c.IncludeHeaders == nil { c.IncludeHeaders = []string{} } if c.InputCreationMethod == nil { c.InputCreationMethod = defaultInput } return nil } func defaultInput(ctx fiber.Ctx) (map[string]interface{}, error) { input := map[string]interface{}{ "method": ctx.Method(), "path": ctx.Path(), } return input, nil } ================================================ FILE: v3/opa/fiber_test.go ================================================ package opa import ( "bytes" "errors" "io" "net/http/httptest" "testing" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" "github.com/stretchr/testify/assert" ) func TestPanicWhenRegoQueryEmpty(t *testing.T) { app := fiber.New() assert.Panics(t, func() { app.Use(New(Config{})) }) } func TestDefaultDeniedStatusCode400WhenConfigDeniedStatusCodeEmpty(t *testing.T) { app := fiber.New() module := ` package example.authz import future.keywords default allow := false ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), DeniedResponseMessage: "not allowed", } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 400, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "not allowed", utils.UnsafeString(readedBytes)) } func TestOpaNotAllowedRegoPolicyShouldReturnConfigDeniedStatusCode(t *testing.T) { app := fiber.New() module := ` package example.authz import future.keywords default allow := false ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), DeniedStatusCode: 401, DeniedResponseMessage: "not allowed", } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, 401, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "not allowed", utils.UnsafeString(readedBytes)) } func TestOpaRequestMethodRegoPolicyShouldReturnConfigDeniedStatusCode(t *testing.T) { app := fiber.New() module := ` package example.authz default allow := false allow if { input.method == "GET" } ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), DeniedStatusCode: fiber.StatusMethodNotAllowed, DeniedResponseMessage: "method not allowed", } app.Use(New(cfg)) app.Get("/", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("POST", "/", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, fiber.StatusMethodNotAllowed, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "method not allowed", utils.UnsafeString(readedBytes)) } func TestOpaRequestPathRegoPolicyShouldReturnOK(t *testing.T) { app := fiber.New() module := ` package example.authz default allow := false allow if { input.path == "/path" } ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), DeniedStatusCode: fiber.StatusOK, DeniedResponseMessage: "OK", } app.Use(New(cfg)) app.Post("/path", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("POST", "/path", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, fiber.StatusOK, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "OK", utils.UnsafeString(readedBytes)) } func TestOpaQueryStringRegoPolicyShouldReturnOK(t *testing.T) { app := fiber.New() module := ` package example.authz import future.keywords.in default allow := false allow if { input.query == {"testKey": ["testVal"]} } ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), IncludeQueryString: true, DeniedStatusCode: fiber.StatusBadRequest, DeniedResponseMessage: "bad request", } app.Use(New(cfg)) app.Get("/test", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/test?testKey=testVal", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, fiber.StatusOK, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "OK", utils.UnsafeString(readedBytes)) } func TestOpaRequestHeadersRegoPolicyShouldReturnOK(t *testing.T) { app := fiber.New() module := ` package example.authz import future.keywords.in default allow := false allow if { input.headers == {"testHeaderKey": "testHeaderVal"} } ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), IncludeQueryString: true, DeniedStatusCode: fiber.StatusBadRequest, DeniedResponseMessage: "bad request", IncludeHeaders: []string{"testHeaderKey"}, } app.Use(New(cfg)) app.Get("/headers", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/headers", nil) r.Header.Set("testHeaderKey", "testHeaderVal") resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, fiber.StatusOK, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "OK", utils.UnsafeString(readedBytes)) } func TestOpaRequestWithCustomInput(t *testing.T) { app := fiber.New() module := ` package example.authz default allow := false allow if { input.custom == "test" } ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), IncludeQueryString: true, DeniedStatusCode: fiber.StatusBadRequest, DeniedResponseMessage: "bad request", InputCreationMethod: func(c fiber.Ctx) (map[string]interface{}, error) { return map[string]interface{}{ "custom": "test", }, nil }, } app.Use(New(cfg)) app.Get("/headers", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/headers", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, fiber.StatusOK, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "OK", utils.UnsafeString(readedBytes)) } func TestOpaRequestWithCustomInputError(t *testing.T) { app := fiber.New() module := ` package example.authz default allow := false allow if { input.custom == "test" } ` cfg := Config{ RegoQuery: "data.example.authz.allow", RegoPolicy: bytes.NewBufferString(module), IncludeQueryString: true, DeniedStatusCode: fiber.StatusBadRequest, DeniedResponseMessage: "bad request", InputCreationMethod: func(c fiber.Ctx) (map[string]interface{}, error) { return nil, errors.New("test error") }, } app.Use(New(cfg)) app.Get("/headers", func(ctx fiber.Ctx) error { return ctx.SendStatus(200) }) r := httptest.NewRequest("GET", "/headers", nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) readedBytes, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Equal(t, "test error", utils.UnsafeString(readedBytes)) } func TestFillAndValidate(t *testing.T) { cfg := Config{} err := cfg.fillAndValidate() assert.Error(t, err) cfg = Config{ RegoPolicy: bytes.NewBufferString("test"), } err = cfg.fillAndValidate() assert.Error(t, err) cfg = Config{ RegoPolicy: bytes.NewBufferString("test"), RegoQuery: "test", } err = cfg.fillAndValidate() assert.NoError(t, err) assert.Equal(t, cfg.DeniedStatusCode, fiber.StatusBadRequest) assert.Equal(t, cfg.DeniedResponseMessage, fiber.ErrBadRequest.Error()) assert.IsType(t, cfg.InputCreationMethod, InputCreationFunc(nil)) assert.IsType(t, cfg.IncludeHeaders, []string(nil)) } ================================================ FILE: v3/opa/go.mod ================================================ module github.com/gofiber/contrib/v3/opa go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/open-policy-agent/opa v1.15.2 github.com/stretchr/testify v1.11.1 ) require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/dsig v1.3.0 // indirect github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect github.com/lestrrat-go/jwx/v3 v3.1.0 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect github.com/valyala/fastjson v1.6.10 // indirect github.com/vektah/gqlparser/v2 v2.5.32 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: v3/opa/go.sum ================================================ github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE= github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w= github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/dsig v1.3.0 h1:phjMOCXvYzhuIgn7Voe2rex8z166vGfxRxmqM25P9/Q= github.com/lestrrat-go/dsig v1.3.0/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc= github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM= github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= github.com/lestrrat-go/jwx/v3 v3.1.0 h1:AyyLtxc0QM75F75JroWgt1phwC7X+wOb3XKhH7XBZWw= github.com/lestrrat-go/jwx/v3 v3.1.0/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/open-policy-agent/opa v1.15.2 h1:dS9q+0Yvruq/VNvWJc5qCvCchn715OWc3HLHXn/UCCc= github.com/open-policy-agent/opa v1.15.2/go.mod h1:c6SN+7jSsUcKJLQc5P4yhwx8YYDRbjpAiGkBOTqxaa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: v3/otel/README.md ================================================ --- id: otel --- # OTel ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*otel*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20otel/badge.svg) [OpenTelemetry](https://opentelemetry.io/) support for Fiber. This package is listed on the [OpenTelemetry Registry](https://opentelemetry.io/registry/instrumentation-go-fiber/). **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/contrib/v3/otel ``` ## Signature ```go otel.Middleware(opts ...otel.Option) fiber.Handler ``` ## Config You can configure the middleware using functional parameters | Function | Argument Type | Description | Default | | :------------------------ | :-------------------------------- | :--------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | | `WithNext` | `func(fiber.Ctx) bool` | Define a function to skip this middleware when returned true .| nil | | `WithTracerProvider` | `oteltrace.TracerProvider` | Specifies a tracer provider to use for creating a tracer. | nil - the global tracer provider is used | | `WithMeterProvider` | `otelmetric.MeterProvider` | Specifies a meter provider to use for reporting. | nil - the global meter provider is used | | `WithPort` | `int` | Specifies the value to use when setting the `server.port` attribute on metrics/spans. | Defaults to (`80` for `http`, `443` for `https`) | | `WithPropagators` | `propagation.TextMapPropagator` | Specifies propagators to use for extracting information from the HTTP requests. | If none are specified, global ones will be used | | (❌ **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. | - | | `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 | | `WithCustomAttributes` | `func(fiber.Ctx) []attribute.KeyValue` | Define a function to add custom attributes to the span. | nil | | `WithCustomMetricAttributes` | `func(fiber.Ctx) []attribute.KeyValue` | Define a function to add custom attributes to the metrics. | nil | | `WithClientIP` | `bool` | Specifies whether to collect the client's IP address from the request. | true | | (⚠️ **Deprecated**) `WithCollectClientIP` | `bool` | Deprecated alias for `WithClientIP`. | true | | `WithoutMetrics` | `bool` | Disables metrics collection when set to true. | false | ## Usage Please refer to [example](./example) ## Metrics Notes - `http.server.request.size` and `http.server.response.size` are measured without buffering full streamed bodies into memory. - For streamed responses, size is recorded when the stream reaches EOF. - For `text/event-stream` responses (SSE), response body size is not recorded. ## Example ```go package main import ( "context" "errors" "log" "go.opentelemetry.io/otel/sdk/resource" "github.com/gofiber/fiber/v3" fiberotel "github.com/gofiber/contrib/v3/otel" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" //"go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/propagation" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.39.0" oteltrace "go.opentelemetry.io/otel/trace" ) var tracer = otel.Tracer("fiber-server") func main() { tp := initTracer() defer func() { if err := tp.Shutdown(context.Background()); err != nil { log.Printf("Error shutting down tracer provider: %v", err) } }() app := fiber.New() app.Use(fiberotel.Middleware()) app.Get("/error", func(ctx fiber.Ctx) error { return errors.New("abc") }) app.Get("/users/:id", func(c fiber.Ctx) error { id := c.Params("id") name := getUser(c.Context(), id) return c.JSON(fiber.Map{"id": id, "name": name}) }) log.Fatal(app.Listen(":3000")) } func initTracer() *sdktrace.TracerProvider { exporter, err := stdout.New(stdout.WithPrettyPrint()) if err != nil { log.Fatal(err) } tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithBatcher(exporter), sdktrace.WithResource( resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String("my-service"), )), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) return tp } func getUser(ctx context.Context, id string) string { _, span := tracer.Start(ctx, "getUser", oteltrace.WithAttributes(attribute.String("id", id))) defer span.End() if id == "123" { return "otel tester" } return "unknown" } ``` ================================================ FILE: v3/otel/config.go ================================================ package otel import ( "github.com/gofiber/fiber/v3" "go.opentelemetry.io/otel/attribute" otelmetric "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" oteltrace "go.opentelemetry.io/otel/trace" ) // config is used to configure the Fiber middleware. type config struct { Next func(fiber.Ctx) bool TracerProvider oteltrace.TracerProvider MeterProvider otelmetric.MeterProvider Port *int Propagators propagation.TextMapPropagator SpanNameFormatter func(fiber.Ctx) string CustomAttributes func(fiber.Ctx) []attribute.KeyValue CustomMetricAttributes func(fiber.Ctx) []attribute.KeyValue clientIP bool withoutMetrics bool } // Option specifies instrumentation configuration options. type Option interface { apply(*config) } type optionFunc func(*config) func (o optionFunc) apply(c *config) { o(c) } // WithNext takes a function that will be called on every // request, the middleware will be skipped if returning true func WithNext(f func(ctx fiber.Ctx) bool) Option { return optionFunc(func(cfg *config) { cfg.Next = f }) } // WithPropagators specifies propagators to use for extracting // information from the HTTP requests. If none are specified, global // ones will be used. func WithPropagators(propagators propagation.TextMapPropagator) Option { return optionFunc(func(cfg *config) { cfg.Propagators = propagators }) } // WithTracerProvider specifies a tracer provider to use for creating a tracer. // If none is specified, the global provider is used. func WithTracerProvider(provider oteltrace.TracerProvider) Option { return optionFunc(func(cfg *config) { cfg.TracerProvider = provider }) } // WithMeterProvider specifies a meter provider to use for reporting. // If none is specified, the global provider is used. func WithMeterProvider(provider otelmetric.MeterProvider) Option { return optionFunc(func(cfg *config) { cfg.MeterProvider = provider }) } // WithSpanNameFormatter takes a function that will be called on every // request and the returned string will become the Span Name func WithSpanNameFormatter(f func(ctx fiber.Ctx) string) Option { return optionFunc(func(cfg *config) { cfg.SpanNameFormatter = f }) } // WithPort specifies the value to use when setting the `server.port` // attribute on metrics/spans. Attribute is "Conditionally Required: If not // default (`80` for `http`, `443` for `https`). func WithPort(port int) Option { return optionFunc(func(cfg *config) { cfg.Port = &port }) } // WithCustomAttributes specifies a function that will be called on every // request and the returned attributes will be added to the span. func WithCustomAttributes(f func(ctx fiber.Ctx) []attribute.KeyValue) Option { return optionFunc(func(cfg *config) { cfg.CustomAttributes = f }) } // WithCustomMetricAttributes specifies a function that will be called on every // request and the returned attributes will be added to the metrics. func WithCustomMetricAttributes(f func(ctx fiber.Ctx) []attribute.KeyValue) Option { return optionFunc(func(cfg *config) { cfg.CustomMetricAttributes = f }) } // WithClientIP specifies whether to collect the client's IP address // from the request. This is enabled by default. func WithClientIP(collect bool) Option { return optionFunc(func(cfg *config) { cfg.clientIP = collect }) } // WithCollectClientIP is deprecated and kept for backwards compatibility. // Deprecated: use WithClientIP instead. func WithCollectClientIP(collect bool) Option { return WithClientIP(collect) } // WithoutMetrics disables metrics collection when set to true func WithoutMetrics(withoutMetrics bool) Option { return optionFunc(func(cfg *config) { cfg.withoutMetrics = withoutMetrics }) } ================================================ FILE: v3/otel/doc.go ================================================ // Package otel instruments the github.com/gofiber/fiber package. // (https://github.com/gofiber/fiber). // // Currently, only the routing of a received message can be instrumented. To do // so, use the Middleware function. package otel // import "github.com/gofiber/contrib/v3/otel" ================================================ FILE: v3/otel/example/Dockerfile ================================================ FROM golang:alpine AS base COPY . /src/ WORKDIR /src/instrumentation/github.com/gofiber/fiber/otelefiber/example FROM base AS fiber-server RUN go install ./server.go CMD ["/go/bin/server"] ================================================ FILE: v3/otel/example/README.md ================================================ --- id: otel-example --- # Example An HTTP server using gofiber fiber and instrumentation. The server has a `/users/:id` endpoint. The server generates span information to `stdout`. These instructions expect you have [docker-compose](https://docs.docker.com/compose/) installed. **Compatible with Fiber v3.** ## 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. Bring up the `fiber-server` and `fiber-client` services to run the example: ```sh docker-compose up --detach fiber-server fiber-client ``` The `fiber-client` service sends just one HTTP request to `fiber-server` and then exits. View the span generated by `fiber-server` in the logs: ```sh docker-compose logs fiber-server ``` Shut down the services when you are finished with the example: ```sh docker-compose down ``` ================================================ FILE: v3/otel/example/docker-compose.yml ================================================ version: "3.7" services: fiber-client: image: golang:alpine networks: - example command: - "/bin/sh" - "-c" - "wget http://fiber-server:3000/users/123 && cat 123" depends_on: - fiber-server fiber-server: build: dockerfile: $PWD/Dockerfile ports: - "3000:80" command: - "/bin/sh" - "-c" - "/go/bin/server" networks: - example networks: example: ================================================ FILE: v3/otel/example/go.mod ================================================ module github.com/gofiber/contrib/v3/otel/example go 1.25.0 replace github.com/gofiber/contrib/v3/otel => ../ require ( github.com/gofiber/contrib/v3/otel v1.0.0 github.com/gofiber/fiber/v3 v3.1.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect ) ================================================ FILE: v3/otel/example/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.4 h1:WwAxUA7L4MW2DjdEHF234lfqvBqd2vYYuBtA9TJq2ec= github.com/gofiber/utils/v2 v2.0.4/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib v1.43.0 h1:rv+pngknCr4qpZDxSpEvEoRioutgfbkk82x6MChJQ3U= go.opentelemetry.io/contrib v1.43.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/otel/example/server.go ================================================ package main import ( "context" "errors" "log" "go.opentelemetry.io/otel/sdk/resource" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/otel" otelApi "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" //"go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/propagation" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" oteltrace "go.opentelemetry.io/otel/trace" ) var tracer = otelApi.Tracer("fiber-server") func main() { tp := initTracer() defer func() { if err := tp.Shutdown(context.Background()); err != nil { log.Printf("Error shutting down tracer provider: %v", err) } }() app := fiber.New() // customise span name //app.Use(otel.Middleware(otel.WithSpanNameFormatter(func(ctx fiber.Ctx) string { // return fmt.Sprintf("%s - %s", ctx.Method(), ctx.Route().Path) //}))) app.Use(otel.Middleware()) app.Get("/error", func(ctx fiber.Ctx) error { return errors.New("abc") }) app.Get("/users/:id", func(c fiber.Ctx) error { id := c.Params("id") name := getUser(c, id) return c.JSON(fiber.Map{"id": id, name: name}) }) log.Fatal(app.Listen(":3000")) } func initTracer() *sdktrace.TracerProvider { exporter, err := stdout.New(stdout.WithPrettyPrint()) //exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces"))) if err != nil { log.Fatal(err) } tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.AlwaysSample()), sdktrace.WithBatcher(exporter), sdktrace.WithResource( resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String("my-service"), )), ) otelApi.SetTracerProvider(tp) otelApi.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) return tp } func getUser(ctx context.Context, id string) string { _, span := tracer.Start(ctx, "getUser", oteltrace.WithAttributes(attribute.String("id", id))) defer span.End() if id == "123" { return "otel tester" } return "unknown" } ================================================ FILE: v3/otel/fiber.go ================================================ package otel import ( "context" "io" "net/http" "sync" "sync/atomic" "time" "github.com/gofiber/contrib/v3/otel/internal" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" otelcontrib "go.opentelemetry.io/contrib" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.39.0" oteltrace "go.opentelemetry.io/otel/trace" ) const ( tracerKey = "gofiber-contrib-tracer-fiber" instrumentationName = "github.com/gofiber/contrib/v3/otel" MetricNameHTTPServerRequestDuration = "http.server.request.duration" MetricNameHTTPServerRequestBodySize = "http.server.request.body.size" MetricNameHTTPServerResponseBodySize = "http.server.response.body.size" MetricNameHTTPServerActiveRequests = "http.server.active_requests" // Unit constants for deprecated metric units UnitDimensionless = "1" UnitBytes = "By" UnitSeconds = "s" // Deprecated: use MetricNameHTTPServerRequestDuration. MetricNameHttpServerDuration = MetricNameHTTPServerRequestDuration // Deprecated: use MetricNameHTTPServerRequestBodySize. MetricNameHttpServerRequestSize = MetricNameHTTPServerRequestBodySize // Deprecated: use MetricNameHTTPServerResponseBodySize. MetricNameHttpServerResponseSize = MetricNameHTTPServerResponseBodySize // Deprecated: use MetricNameHTTPServerActiveRequests. MetricNameHttpServerActiveRequests = MetricNameHTTPServerActiveRequests // Deprecated: kept for backward compatibility with legacy millisecond-based metrics. // New duration metrics use UnitSeconds. UnitMilliseconds = "ms" ) type bodyStreamSizeReader struct { reader io.Reader onEOF func(read int64) read int64 eof sync.Once } func (b *bodyStreamSizeReader) Read(p []byte) (n int, err error) { n, err = b.reader.Read(p) if n > 0 { atomic.AddInt64(&b.read, int64(n)) } if err == io.EOF && b.onEOF != nil { read := atomic.LoadInt64(&b.read) b.eof.Do(func() { b.onEOF(read) }) } return n, err } func (b *bodyStreamSizeReader) Close() error { closer, ok := b.reader.(io.Closer) if !ok { return nil } return closer.Close() } func detachedMetricContext(ctx context.Context) context.Context { detached := context.Background() if spanContext := oteltrace.SpanContextFromContext(ctx); spanContext.IsValid() { detached = oteltrace.ContextWithSpanContext(detached, spanContext) } if bg := baggage.FromContext(ctx); bg.Len() > 0 { detached = baggage.ContextWithBaggage(detached, bg) } return detached } // Middleware returns fiber handler which will trace incoming requests. func Middleware(opts ...Option) fiber.Handler { cfg := config{ clientIP: true, } for _, opt := range opts { opt.apply(&cfg) } if cfg.TracerProvider == nil { cfg.TracerProvider = otel.GetTracerProvider() } tracer := cfg.TracerProvider.Tracer( instrumentationName, oteltrace.WithInstrumentationVersion(otelcontrib.Version()), ) var httpServerDuration metric.Float64Histogram var httpServerRequestSize metric.Int64Histogram var httpServerResponseSize metric.Int64Histogram var httpServerActiveRequests metric.Int64UpDownCounter if !cfg.withoutMetrics { if cfg.MeterProvider == nil { cfg.MeterProvider = otel.GetMeterProvider() } meter := cfg.MeterProvider.Meter( instrumentationName, metric.WithInstrumentationVersion(otelcontrib.Version()), ) var err error httpServerDuration, err = meter.Float64Histogram(MetricNameHTTPServerRequestDuration, metric.WithUnit(UnitSeconds), metric.WithDescription("Duration of HTTP server requests.")) if err != nil { otel.Handle(err) } httpServerRequestSize, err = meter.Int64Histogram(MetricNameHTTPServerRequestBodySize, metric.WithUnit(UnitBytes), metric.WithDescription("Size of HTTP server request bodies.")) if err != nil { otel.Handle(err) } httpServerResponseSize, err = meter.Int64Histogram(MetricNameHTTPServerResponseBodySize, metric.WithUnit(UnitBytes), metric.WithDescription("Size of HTTP server response bodies.")) if err != nil { otel.Handle(err) } httpServerActiveRequests, err = meter.Int64UpDownCounter(MetricNameHTTPServerActiveRequests, metric.WithUnit(UnitDimensionless), metric.WithDescription("Number of active HTTP server requests.")) if err != nil { otel.Handle(err) } } if cfg.Propagators == nil { cfg.Propagators = otel.GetTextMapPropagator() } if cfg.SpanNameFormatter == nil { cfg.SpanNameFormatter = defaultSpanNameFormatter } return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } fiber.StoreInContext(c, tracerKey, tracer) savedCtx, cancel := context.WithCancel(c.Context()) start := time.Now() requestMetricsAttrs := httpServerMetricAttributesFromRequest(c, cfg) if !cfg.withoutMetrics { httpServerActiveRequests.Add(savedCtx, 1, metric.WithAttributes(requestMetricsAttrs...)) } responseMetricAttrs := make([]attribute.KeyValue, len(requestMetricsAttrs)) copy(responseMetricAttrs, requestMetricsAttrs) request := c.Request() isRequestBodyStream := request.IsBodyStream() requestSize := int64(0) var requestBodyStreamSizeReader *bodyStreamSizeReader if isRequestBodyStream && !cfg.withoutMetrics { requestBodyStream := request.BodyStream() if requestBodyStream != nil { requestBodyStreamSizeReader = &bodyStreamSizeReader{reader: requestBodyStream} request.SetBodyStream(requestBodyStreamSizeReader, -1) } } else { requestSize = int64(len(request.Body())) } reqHeader := make(http.Header) for header, values := range c.GetReqHeaders() { for _, value := range values { reqHeader.Add(header, value) } } ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(reqHeader)) opts := []oteltrace.SpanStartOption{ oteltrace.WithAttributes(httpServerTraceAttributesFromRequest(c, cfg)...), oteltrace.WithSpanKind(oteltrace.SpanKindServer), } // temporary set to c.Path() first // update with c.Route().Path after c.Next() is called // to get pathRaw spanName := utils.CopyString(c.Path()) ctx, span := tracer.Start(ctx, spanName, opts...) defer span.End() // pass the span through userContext c.SetContext(ctx) // serve the request to the next middleware if err := c.Next(); err != nil { span.RecordError(err) // invokes the registered HTTP error handler // to get the correct response status code _ = c.App().Config().ErrorHandler(c, err) } // extract common attributes from response responseAttrs := []attribute.KeyValue{ semconv.HTTPResponseStatusCode(c.Response().StatusCode()), semconv.HTTPRouteKey.String(c.Route().Path), // no need to copy c.Route().Path: route strings should be immutable across app lifecycle } response := c.Response() isSSE := c.GetRespHeader("Content-Type") == "text/event-stream" responseSize := int64(0) isResponseBodyStream := response.IsBodyStream() if !isResponseBodyStream && !isSSE { responseSize = int64(len(response.Body())) } if isResponseBodyStream && !isSSE && !cfg.withoutMetrics { responseBodyStream := response.BodyStream() if responseBodyStream != nil { responseMetricAttrsWithResponse := append(responseMetricAttrs, responseAttrs...) responseMetricsCtx := detachedMetricContext(savedCtx) responseBodyStreamReader := &bodyStreamSizeReader{ reader: responseBodyStream, onEOF: func(read int64) { httpServerResponseSize.Record(responseMetricsCtx, read, metric.WithAttributes(responseMetricAttrsWithResponse...)) }, } response.SetBodyStream(responseBodyStreamReader, -1) } else { isResponseBodyStream = false } } defer func() { responseMetricAttrs = append(responseMetricAttrs, responseAttrs...) if requestBodyStreamSizeReader != nil { requestSize = atomic.LoadInt64(&requestBodyStreamSizeReader.read) } if !cfg.withoutMetrics { httpServerActiveRequests.Add(savedCtx, -1, metric.WithAttributes(requestMetricsAttrs...)) httpServerDuration.Record(savedCtx, time.Since(start).Seconds(), metric.WithAttributes(responseMetricAttrs...)) httpServerRequestSize.Record(savedCtx, requestSize, metric.WithAttributes(responseMetricAttrs...)) if !isResponseBodyStream { httpServerResponseSize.Record(savedCtx, responseSize, metric.WithAttributes(responseMetricAttrs...)) } } c.SetContext(savedCtx) cancel() }() if !isResponseBodyStream { span.SetAttributes(append(responseAttrs, semconv.HTTPResponseBodySizeKey.Int64(responseSize))...) } else { span.SetAttributes(responseAttrs...) } span.SetName(cfg.SpanNameFormatter(c)) spanStatus, spanMessage := internal.SpanStatusFromHTTPStatusCodeAndSpanKind(c.Response().StatusCode(), oteltrace.SpanKindServer) span.SetStatus(spanStatus, spanMessage) //Propagate tracing context as headers in outbound response tracingHeaders := make(propagation.HeaderCarrier) cfg.Propagators.Inject(c.Context(), tracingHeaders) for _, headerKey := range tracingHeaders.Keys() { c.Set(headerKey, tracingHeaders.Get(headerKey)) } return nil } } // defaultSpanNameFormatter is the default formatter for spans created with the fiber // integration. Returns the route pathRaw func defaultSpanNameFormatter(ctx fiber.Ctx) string { return ctx.Route().Path } ================================================ FILE: v3/otel/fiber_context_test.go ================================================ package otel import ( "net/http" "net/http/httptest" "testing" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" oteltrace "go.opentelemetry.io/otel/trace" ) func TestMiddleware_StoreTracerInContextWithPassLocalsToContext(t *testing.T) { app := fiber.New(fiber.Config{PassLocalsToContext: true}) app.Use(Middleware()) app.Get("/", func(c fiber.Ctx) error { tracerFromContext, ok := fiber.ValueFromContext[oteltrace.Tracer](c.Context(), tracerKey) require.True(t, ok) require.NotNil(t, tracerFromContext) return c.SendStatus(http.StatusOK) }) resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) } ================================================ FILE: v3/otel/go.mod ================================================ module github.com/gofiber/contrib/v3/otel go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/contrib v1.43.0 go.opentelemetry.io/contrib/propagators/b3 v1.43.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/otel/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib v1.43.0 h1:rv+pngknCr4qpZDxSpEvEoRioutgfbkk82x6MChJQ3U= go.opentelemetry.io/contrib v1.43.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A= go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/otel/internal/http.go ================================================ package internal import ( "fmt" "net/http" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) // SpanStatusFromHTTPStatusCodeAndSpanKind generates a status code and a message // as specified by the OpenTelemetry specification for a span. // Exclude 4xx for SERVER to set the appropriate status. func SpanStatusFromHTTPStatusCodeAndSpanKind(code int, spanKind trace.SpanKind) (codes.Code, string) { // This code block ignores the HTTP 306 status code. The 306 status code is no longer in use. if http.StatusText(code) == "" { return codes.Error, fmt.Sprintf("Invalid HTTP status code %d", code) } if (code >= http.StatusContinue && code < http.StatusBadRequest) || (spanKind == trace.SpanKindServer && isCode4xx(code)) { return codes.Unset, "" } return codes.Error, "" } func isCode4xx(code int) bool { return code >= http.StatusBadRequest && code <= http.StatusUnavailableForLegalReasons } ================================================ FILE: v3/otel/internal/http_test.go ================================================ package internal import ( "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/codes" oteltrace "go.opentelemetry.io/otel/trace" "net/http" "testing" ) func TestIsCode4xxIsNotValid(t *testing.T) { response := isCode4xx(http.StatusOK) assert.False(t, response) } func TestIsCode4xxIsValid(t *testing.T) { response := isCode4xx(http.StatusNotFound) assert.True(t, response) } func TestStatusErrorWithMessage(t *testing.T) { spanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(600, oteltrace.SpanKindClient) assert.Equal(t, codes.Error, spanStatus) assert.Equal(t, "Invalid HTTP status code 600", spanMessage) } func TestStatusErrorWithMessageForIgnoredHTTPCode(t *testing.T) { spanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(306, oteltrace.SpanKindClient) assert.Equal(t, codes.Error, spanStatus) assert.Equal(t, "Invalid HTTP status code 306", spanMessage) } func TestStatusErrorWhenHTTPCode5xx(t *testing.T) { spanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(http.StatusInternalServerError, oteltrace.SpanKindServer) assert.Equal(t, codes.Error, spanStatus) assert.Equal(t, "", spanMessage) } func TestStatusUnsetWhenServerSpanAndBadRequest(t *testing.T) { spanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(http.StatusBadRequest, oteltrace.SpanKindServer) assert.Equal(t, codes.Unset, spanStatus) assert.Equal(t, "", spanMessage) } func TestStatusUnset(t *testing.T) { spanStatus, spanMessage := SpanStatusFromHTTPStatusCodeAndSpanKind(http.StatusOK, oteltrace.SpanKindClient) assert.Equal(t, codes.Unset, spanStatus) assert.Equal(t, "", spanMessage) } ================================================ FILE: v3/otel/otel_test/fiber_test.go ================================================ package otel_test import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/http/httptest" "testing" "time" fiberotel "github.com/gofiber/contrib/v3/otel" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" otelcontrib "go.opentelemetry.io/contrib" b3prop "go.opentelemetry.io/contrib/propagators/b3" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" semconv "go.opentelemetry.io/otel/semconv/v1.39.0" oteltrace "go.opentelemetry.io/otel/trace" ) const instrumentationName = "github.com/gofiber/contrib/v3/otel" func TestChildSpanFromGlobalTracer(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) app := fiber.New() app.Use(fiberotel.Middleware()) app.Get("/user/:id", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) resp, err := app.Test(httptest.NewRequest("GET", "/user/123", nil)) require.NoError(t, err) require.NotNil(t, resp) spans := sr.Ended() require.Len(t, spans, 1) } func TestChildSpanFromCustomTracer(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) app := fiber.New() app.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider))) app.Get("/user/:id", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) resp, err := app.Test(httptest.NewRequest("GET", "/user/123", nil)) require.NoError(t, err) require.NotNil(t, resp) spans := sr.Ended() require.Len(t, spans, 1) } func TestSkipWithNext(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) app := fiber.New() app.Use(fiberotel.Middleware(fiberotel.WithNext(func(c fiber.Ctx) bool { return c.Path() == "/health" }))) app.Get("/health", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) resp, err := app.Test(httptest.NewRequest("GET", "/health", nil)) require.NoError(t, err) require.NotNil(t, resp) spans := sr.Ended() require.Len(t, spans, 0) } func TestTrace200(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) app := fiber.New() app.Use( fiberotel.Middleware(fiberotel.WithTracerProvider(provider)), ) app.Get("/user/:id", func(ctx fiber.Ctx) error { id := ctx.Params("id") return ctx.SendString(id) }) r := httptest.NewRequest("GET", "/user/123", nil) resp, err := app.Test(r, fiber.TestConfig{Timeout: 3 * time.Second}) require.NoError(t, err) require.NotNil(t, resp) // do and verify the request require.Equal(t, http.StatusOK, resp.StatusCode) spans := sr.Ended() require.Len(t, spans, 1) // verify traces look good span := spans[0] attr := span.Attributes() assert.Equal(t, "/user/:id", span.Name()) assert.Equal(t, oteltrace.SpanKindServer, span.SpanKind()) assert.Contains(t, attr, attribute.String("server.address", r.Host)) assert.Contains(t, attr, attribute.Int("http.response.status_code", http.StatusOK)) assert.Contains(t, attr, attribute.String("http.request.method", "GET")) assert.Contains(t, attr, attribute.String("url.path", "/user/123")) assert.Contains(t, attr, attribute.String("http.route", "/user/:id")) } func TestError(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) // setup app := fiber.New() app.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider))) // configure a handler that returns an error and 5xx status code app.Get("/server_err", func(ctx fiber.Ctx) error { return errors.New("oh no") }) resp, err := app.Test(httptest.NewRequest("GET", "/server_err", nil)) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) // verify the errors and status are correct spans := sr.Ended() require.Len(t, spans, 1) span := spans[0] attr := span.Attributes() assert.Equal(t, "/server_err", span.Name()) assert.Contains(t, attr, attribute.Int("http.response.status_code", http.StatusInternalServerError)) assert.Equal(t, attribute.StringValue("oh no"), span.Events()[0].Attributes[1].Value) // server errors set the status assert.Equal(t, codes.Error, span.Status().Code) } func TestErrorOnlyHandledOnce(t *testing.T) { timesHandlingError := 0 app := fiber.New(fiber.Config{ ErrorHandler: func(ctx fiber.Ctx, err error) error { timesHandlingError++ return fiber.NewError(http.StatusInternalServerError, err.Error()) }, }) app.Use(fiberotel.Middleware()) app.Get("/", func(ctx fiber.Ctx) error { return errors.New("mock error") }) resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, 1, timesHandlingError) } func TestGetSpanNotInstrumented(t *testing.T) { var gotSpan oteltrace.Span app := fiber.New() app.Get("/ping", func(ctx fiber.Ctx) error { // Assert we don't have a span on the context. gotSpan = oteltrace.SpanFromContext(ctx) return ctx.SendString("ok") }) resp, err := app.Test(httptest.NewRequest("GET", "/ping", nil)) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, http.StatusOK, resp.StatusCode) ok := !gotSpan.SpanContext().IsValid() assert.True(t, ok) } func TestPropagationWithGlobalPropagators(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) otel.SetTextMapPropagator(propagation.TraceContext{}) defer otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator()) r := httptest.NewRequest("GET", "/user/123", nil) ctx, pspan := provider.Tracer(instrumentationName).Start(context.Background(), "test") otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(r.Header)) app := fiber.New() app.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider))) app.Get("/user/:id", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) resp, err := app.Test(r) require.NoError(t, err) require.NotNil(t, resp) spans := sr.Ended() require.Len(t, spans, 1) // verify traces look good span := spans[0] assert.Equal(t, pspan.SpanContext().TraceID(), span.SpanContext().TraceID()) assert.Equal(t, pspan.SpanContext().SpanID(), span.Parent().SpanID()) } func TestPropagationWithCustomPropagators(t *testing.T) { sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) otel.SetTracerProvider(provider) b3 := b3prop.New() r := httptest.NewRequest("GET", "/user/123", nil) ctx, pspan := provider.Tracer(instrumentationName).Start(context.Background(), "test") b3.Inject(ctx, propagation.HeaderCarrier(r.Header)) app := fiber.New() app.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider), fiberotel.WithPropagators(b3))) app.Get("/user/:id", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) resp, err := app.Test(r) require.NoError(t, err) require.NotNil(t, resp) spans := sr.Ended() require.Len(t, spans, 1) mspan := spans[0] assert.Equal(t, pspan.SpanContext().TraceID(), mspan.SpanContext().TraceID()) assert.Equal(t, pspan.SpanContext().SpanID(), mspan.Parent().SpanID()) } func TestHasBasicAuth(t *testing.T) { testCases := []struct { desc string auth string user string valid bool }{ { desc: "valid header", auth: "Basic dXNlcjpwYXNzd29yZA==", user: "user", valid: true, }, { desc: "invalid header", auth: "Bas", }, { desc: "invalid basic header", auth: "Basic 12345", }, { desc: "no header", }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { val, valid := fiberotel.HasBasicAuth(tC.auth) assert.Equal(t, tC.user, val) assert.Equal(t, tC.valid, valid) }) } } func TestMetric(t *testing.T) { reader := metric.NewManualReader() provider := metric.NewMeterProvider(metric.WithReader(reader)) port := 8080 route := "/foo" app := fiber.New() app.Use( fiberotel.Middleware( fiberotel.WithMeterProvider(provider), fiberotel.WithPort(port), ), ) app.Get(route, func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusOK) }) r := httptest.NewRequest(http.MethodGet, route, nil) resp, err := app.Test(r) require.NoError(t, err) require.NotNil(t, resp) metrics := metricdata.ResourceMetrics{} err = reader.Collect(context.Background(), &metrics) assert.NoError(t, err) assert.Len(t, metrics.ScopeMetrics, 1) requestAttrs := []attribute.KeyValue{ semconv.NetworkProtocolName("http"), semconv.NetworkProtocolVersion(fmt.Sprintf("1.%d", r.ProtoMinor)), semconv.URLScheme("http"), semconv.HTTPRequestMethodKey.String(http.MethodGet), semconv.ServerAddress(r.Host), semconv.ServerPort(port), } responseAttrs := []attribute.KeyValue{ semconv.HTTPResponseStatusCode(200), semconv.HTTPRouteKey.String(route), } assertScopeMetrics(t, metrics.ScopeMetrics[0], route, requestAttrs, append(requestAttrs, responseAttrs...)) } func assertScopeMetrics(t *testing.T, sm metricdata.ScopeMetrics, route string, requestAttrs []attribute.KeyValue, responseAttrs []attribute.KeyValue) { assert.Equal(t, instrumentation.Scope{ Name: instrumentationName, Version: otelcontrib.Version(), }, sm.Scope) // Duration value is not predictable. m := sm.Metrics[0] assert.Equal(t, fiberotel.MetricNameHTTPServerRequestDuration, m.Name) assert.Equal(t, fiberotel.UnitSeconds, m.Unit) require.IsType(t, m.Data, metricdata.Histogram[float64]{}) hist := m.Data.(metricdata.Histogram[float64]) assert.Equal(t, metricdata.CumulativeTemporality, hist.Temporality) require.Len(t, hist.DataPoints, 1) dp := hist.DataPoints[0] assert.Equal(t, attribute.NewSet(responseAttrs...), dp.Attributes, "attributes") assert.Equal(t, uint64(1), dp.Count, "count") assert.Less(t, dp.Sum, 0.01) // test shouldn't take longer than 10 milliseconds (0.01 seconds) // Request size want := metricdata.Metrics{ Name: fiberotel.MetricNameHTTPServerRequestBodySize, Description: "Size of HTTP server request bodies.", Unit: fiberotel.UnitBytes, Data: getHistogram(0, responseAttrs), } metricdatatest.AssertEqual(t, want, sm.Metrics[1], metricdatatest.IgnoreTimestamp()) // Response size want = metricdata.Metrics{ Name: fiberotel.MetricNameHTTPServerResponseBodySize, Description: "Size of HTTP server response bodies.", Unit: fiberotel.UnitBytes, Data: getHistogram(2, responseAttrs), } metricdatatest.AssertEqual(t, want, sm.Metrics[2], metricdatatest.IgnoreTimestamp()) // Active requests want = metricdata.Metrics{ Name: fiberotel.MetricNameHTTPServerActiveRequests, Description: "Number of active HTTP server requests.", Unit: fiberotel.UnitDimensionless, Data: metricdata.Sum[int64]{ DataPoints: []metricdata.DataPoint[int64]{ {Attributes: attribute.NewSet(requestAttrs...), Value: 0}, }, Temporality: metricdata.CumulativeTemporality, }, } metricdatatest.AssertEqual(t, want, sm.Metrics[3], metricdatatest.IgnoreTimestamp()) } func getHistogram(value float64, attrs []attribute.KeyValue) metricdata.Histogram[int64] { bounds := []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000} bucketCounts := make([]uint64, len(bounds)+1) for i, v := range bounds { if value <= v { bucketCounts[i]++ break } if i == len(bounds)-1 { bounds[i+1]++ break } } extremaValue := metricdata.NewExtrema[int64](int64(value)) return metricdata.Histogram[int64]{ DataPoints: []metricdata.HistogramDataPoint[int64]{ { Attributes: attribute.NewSet(attrs...), Bounds: bounds, BucketCounts: bucketCounts, Count: 1, Min: extremaValue, Max: extremaValue, Sum: int64(value), }, }, Temporality: metricdata.CumulativeTemporality, } } func TestCustomAttributes(t *testing.T) { sr := new(tracetest.SpanRecorder) provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) app := fiber.New() app.Use( fiberotel.Middleware( fiberotel.WithTracerProvider(provider), fiberotel.WithCustomAttributes(func(ctx fiber.Ctx) []attribute.KeyValue { return []attribute.KeyValue{ attribute.Key("http.query_params").String(ctx.Request().URI().QueryArgs().String()), } }), ), ) app.Get("/user/:id", func(ctx fiber.Ctx) error { id := ctx.Params("id") return ctx.SendString(id) }) resp, err := app.Test(httptest.NewRequest("GET", "/user/123?foo=bar", nil), fiber.TestConfig{Timeout: 3 * time.Second}) require.NoError(t, err) require.NotNil(t, resp) // do and verify the request require.Equal(t, http.StatusOK, resp.StatusCode) spans := sr.Ended() require.Len(t, spans, 1) // verify traces look good span := spans[0] attr := span.Attributes() assert.Equal(t, "/user/:id", span.Name()) assert.Equal(t, oteltrace.SpanKindServer, span.SpanKind()) assert.Contains(t, attr, attribute.Int("http.response.status_code", http.StatusOK)) assert.Contains(t, attr, attribute.String("http.request.method", "GET")) assert.Contains(t, attr, attribute.String("url.path", "/user/123")) assert.Contains(t, attr, attribute.String("http.route", "/user/:id")) assert.Contains(t, attr, semconv.URLQuery("foo=bar")) } func TestCustomMetricAttributes(t *testing.T) { reader := metric.NewManualReader() provider := metric.NewMeterProvider(metric.WithReader(reader)) port := 8080 route := "/foo" app := fiber.New() app.Use( fiberotel.Middleware( fiberotel.WithMeterProvider(provider), fiberotel.WithPort(port), fiberotel.WithCustomMetricAttributes(func(ctx fiber.Ctx) []attribute.KeyValue { return []attribute.KeyValue{semconv.URLQuery(ctx.Request().URI().QueryArgs().String())} }), ), ) app.Get(route, func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusOK) }) r := httptest.NewRequest(http.MethodGet, "/foo?foo=bar", nil) resp, err := app.Test(r) require.NoError(t, err) require.NotNil(t, resp) // do and verify the request require.Equal(t, http.StatusOK, resp.StatusCode) metrics := metricdata.ResourceMetrics{} err = reader.Collect(context.Background(), &metrics) assert.NoError(t, err) assert.Len(t, metrics.ScopeMetrics, 1) requestAttrs := []attribute.KeyValue{ semconv.NetworkProtocolName("http"), semconv.NetworkProtocolVersion(fmt.Sprintf("1.%d", r.ProtoMinor)), semconv.HTTPRequestMethodKey.String(http.MethodGet), semconv.URLSchemeKey.String("http"), semconv.ServerAddress(r.Host), semconv.ServerPort(port), semconv.URLQuery("foo=bar"), } responseAttrs := []attribute.KeyValue{ semconv.HTTPResponseStatusCode(200), semconv.HTTPRouteKey.String(route), } assertScopeMetrics(t, metrics.ScopeMetrics[0], route, requestAttrs, append(requestAttrs, responseAttrs...)) } func TestOutboundTracingPropagation(t *testing.T) { sr := new(tracetest.SpanRecorder) provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) app := fiber.New() app.Use(fiberotel.Middleware( fiberotel.WithTracerProvider(provider), fiberotel.WithPropagators(b3prop.New(b3prop.WithInjectEncoding(b3prop.B3MultipleHeader))), )) app.Get("/foo", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) resp, err := app.Test(httptest.NewRequest("GET", "/foo", nil), fiber.TestConfig{Timeout: 3 * time.Second}) require.NoError(t, err) require.NotNil(t, resp) assert.Equal(t, "1", resp.Header.Get("X-B3-Sampled")) assert.NotEmpty(t, resp.Header.Get("X-B3-SpanId")) assert.NotEmpty(t, resp.Header.Get("X-B3-TraceId")) } func TestOutboundTracingPropagationWithInboundContext(t *testing.T) { const spanId = "619907d88b766fb8" const traceId = "813dd2766ff711bf02b60e9883014964" sr := new(tracetest.SpanRecorder) provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) app := fiber.New() app.Use(fiberotel.Middleware( fiberotel.WithTracerProvider(provider), fiberotel.WithPropagators(b3prop.New(b3prop.WithInjectEncoding(b3prop.B3MultipleHeader))), )) app.Get("/foo", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) req := httptest.NewRequest("GET", "/foo", nil) req.Header.Set("X-B3-SpanId", spanId) req.Header.Set("X-B3-TraceId", traceId) req.Header.Set("X-B3-Sampled", "1") resp, err := app.Test(req, fiber.TestConfig{Timeout: 3 * time.Second}) require.NoError(t, err) require.NotNil(t, resp) assert.NotEmpty(t, resp.Header.Get("X-B3-SpanId")) assert.Equal(t, traceId, resp.Header.Get("X-B3-TraceId")) assert.Equal(t, "1", resp.Header.Get("X-B3-Sampled")) } func TestCollectClientIP(t *testing.T) { t.Parallel() optFactories := []struct { name string opt func(bool) fiberotel.Option }{ {name: "WithClientIP", opt: fiberotel.WithClientIP}, {name: "WithCollectClientIP", opt: fiberotel.WithCollectClientIP}, } for _, factory := range optFactories { factory := factory t.Run(factory.name, func(t *testing.T) { t.Parallel() for _, enabled := range []bool{true, false} { enabled := enabled t.Run(fmt.Sprintf("enabled=%t", enabled), func(t *testing.T) { t.Parallel() sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) app := fiber.New() app.Use(fiberotel.Middleware( fiberotel.WithTracerProvider(provider), factory.opt(enabled), )) app.Get("/foo", func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusNoContent) }) req := httptest.NewRequest("GET", "/foo", nil) resp, err := app.Test(req) require.NoError(t, err) require.NotNil(t, resp) spans := sr.Ended() require.Len(t, spans, 1) span := spans[0] attrs := span.Attributes() if enabled { assert.Contains(t, attrs, attribute.String("client.address", "0.0.0.0")) } else { assert.NotContains(t, attrs, attribute.String("client.address", "0.0.0.0")) } }) } }) } } func TestMiddlewarePreservesUserContext(t *testing.T) { type ctxKey string const requestIDKey ctxKey = "request_id" const expectedID = 1234 sr := tracetest.NewSpanRecorder() provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) app := fiber.New() // Middleware that injects a value into the context before otel app.Use(func(c fiber.Ctx) error { ctx := context.WithValue(c.Context(), requestIDKey, expectedID) c.SetContext(ctx) return c.Next() }) app.Use(fiberotel.Middleware(fiberotel.WithTracerProvider(provider))) app.Get("/", func(c fiber.Ctx) error { val := c.Context().Value(requestIDKey) if val == nil { return c.SendString("request_id NOT found in context") } return c.SendString(fmt.Sprintf("request_id from context: %d", val.(int))) }) resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, fmt.Sprintf("request_id from context: %d", expectedID), string(body)) } func TestWithoutMetrics(t *testing.T) { reader := metric.NewManualReader() provider := metric.NewMeterProvider(metric.WithReader(reader)) port := 8080 route := "/foo" app := fiber.New() app.Use( fiberotel.Middleware( fiberotel.WithMeterProvider(provider), fiberotel.WithPort(port), fiberotel.WithoutMetrics(true), ), ) app.Get(route, func(ctx fiber.Ctx) error { return ctx.SendStatus(http.StatusOK) }) r := httptest.NewRequest(http.MethodGet, route, nil) resp, err := app.Test(r) require.NoError(t, err) require.NotNil(t, resp) metrics := metricdata.ResourceMetrics{} err = reader.Collect(context.Background(), &metrics) assert.NoError(t, err) assert.Len(t, metrics.ScopeMetrics, 0, "No metrics should be collected when metrics are disabled") } func TestWithoutMetricsWithStreamResponse(t *testing.T) { reader := metric.NewManualReader() provider := metric.NewMeterProvider(metric.WithReader(reader)) app := fiber.New() app.Use( fiberotel.Middleware( fiberotel.WithMeterProvider(provider), fiberotel.WithoutMetrics(true), ), ) app.Get("/stream", func(ctx fiber.Ctx) error { return ctx.SendStream(bytes.NewReader(make([]byte, 2048))) }) resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/stream", nil)) require.NoError(t, err) require.NotNil(t, resp) body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Len(t, body, 2048) metrics := metricdata.ResourceMetrics{} err = reader.Collect(context.Background(), &metrics) assert.NoError(t, err) assert.Len(t, metrics.ScopeMetrics, 0, "No metrics should be collected when metrics are disabled") } func TestResponseBodySizeWithStream(t *testing.T) { const responseBodySize = 8192 reader := metric.NewManualReader() provider := metric.NewMeterProvider(metric.WithReader(reader)) app := fiber.New() app.Use( fiberotel.Middleware( fiberotel.WithMeterProvider(provider), ), ) app.Get("/stream", func(ctx fiber.Ctx) error { payload := make([]byte, responseBodySize) return ctx.SendStream(bytes.NewReader(payload)) }) resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/stream", nil)) require.NoError(t, err) require.NotNil(t, resp) body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Len(t, body, responseBodySize) metrics := metricdata.ResourceMetrics{} err = reader.Collect(context.Background(), &metrics) require.NoError(t, err) require.Len(t, metrics.ScopeMetrics, 1) var got metricdata.Histogram[int64] for _, m := range metrics.ScopeMetrics[0].Metrics { if m.Name == fiberotel.MetricNameHttpServerResponseSize { var ok bool got, ok = m.Data.(metricdata.Histogram[int64]) require.True(t, ok) break } } require.Len(t, got.DataPoints, 1) assert.Equal(t, int64(responseBodySize), got.DataPoints[0].Sum) } ================================================ FILE: v3/otel/semconv.go ================================================ package otel import ( "bytes" "encoding/base64" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.39.0" ) var ( httpProtocolNameAttr = semconv.NetworkProtocolName("http") http11VersionAttr = semconv.NetworkProtocolVersion("1.1") http10VersionAttr = semconv.NetworkProtocolVersion("1.0") enduserIDKey = attribute.Key("enduser.id") ) func httpServerMetricAttributesFromRequest(c fiber.Ctx, cfg config) []attribute.KeyValue { protocolAttributes := httpNetworkProtocolAttributes(c) attrs := []attribute.KeyValue{ semconv.URLScheme(requestScheme(c)), semconv.ServerAddress(utils.CopyString(c.Hostname())), semconv.HTTPRequestMethodKey.String(utils.CopyString(c.Method())), } attrs = append(attrs, protocolAttributes...) if cfg.Port != nil { attrs = append(attrs, semconv.ServerPort(*cfg.Port)) } if cfg.CustomMetricAttributes != nil { attrs = append(attrs, cfg.CustomMetricAttributes(c)...) } return attrs } func httpServerTraceAttributesFromRequest(c fiber.Ctx, cfg config) []attribute.KeyValue { protocolAttributes := httpNetworkProtocolAttributes(c) attrs := []attribute.KeyValue{ // utils.CopyString: we need to copy the string as fasthttp strings are by default // mutable so it will be unsafe to use in this middleware as it might be used after // the handler returns. semconv.HTTPRequestMethodKey.String(utils.CopyString(c.Method())), semconv.URLScheme(requestScheme(c)), semconv.HTTPRequestBodySize(c.Request().Header.ContentLength()), semconv.URLPath(string(utils.CopyBytes(c.Request().URI().Path()))), semconv.URLQuery(c.Request().URI().QueryArgs().String()), semconv.URLFull(utils.CopyString(c.OriginalURL())), semconv.UserAgentOriginal(string(utils.CopyBytes(c.Request().Header.UserAgent()))), semconv.ServerAddress(utils.CopyString(c.Hostname())), semconv.NetworkTransportTCP, } attrs = append(attrs, protocolAttributes...) if cfg.Port != nil { attrs = append(attrs, semconv.ServerPort(*cfg.Port)) } if username, ok := HasBasicAuth(c.Get(fiber.HeaderAuthorization)); ok { attrs = append(attrs, enduserIDKey.String(username)) } if cfg.clientIP { clientIP := c.IP() if len(clientIP) > 0 { attrs = append(attrs, semconv.ClientAddress(utils.CopyString(clientIP))) } } if cfg.CustomAttributes != nil { attrs = append(attrs, cfg.CustomAttributes(c)...) } return attrs } func httpNetworkProtocolAttributes(c fiber.Ctx) []attribute.KeyValue { httpProtocolAttributes := []attribute.KeyValue{httpProtocolNameAttr} if c.Request().Header.IsHTTP11() { return append(httpProtocolAttributes, http11VersionAttr) } return append(httpProtocolAttributes, http10VersionAttr) } func requestScheme(c fiber.Ctx) string { scheme := c.Request().URI().Scheme() if len(scheme) == 0 { return "http" } return utils.CopyString(string(scheme)) } func HasBasicAuth(auth string) (string, bool) { if auth == "" { return "", false } // Check if the Authorization header is Basic. // Auth schemes are case-insensitive. if len(auth) < 6 || !utils.EqualFold(auth[:6], "Basic ") { return "", false } // Decode the header contents raw, err := base64.StdEncoding.DecodeString(auth[6:]) if err != nil { return "", false } // Check if the decoded credentials are in the correct form // which is "username:password". index := bytes.IndexByte(raw, ':') if index == -1 { return "", false } // Get the username return string(raw[:index]), true } ================================================ FILE: v3/paseto/README.md ================================================ --- id: paseto --- # Paseto ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*paseto*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20paseto/badge.svg) PASETO returns a Web Token (PASETO) auth middleware. - 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. - For invalid token, it returns "401 - Unauthorized" error. - For missing token, it returns "400 - BadRequest" error. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/paseto go get -u github.com/o1egl/paseto ``` ## Signature ```go pasetoware.New(config ...pasetoware.Config) func(fiber.Ctx) error pasetoware.FromContext(ctx any) interface{} ``` `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). ## Config | Property | Type | Description | Default | |:---------------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------| | Next | `func(fiber.Ctx) bool` | Defines a function to skip this middleware when it returns true. | `nil` | | SuccessHandler | `func(fiber.Ctx) error` | SuccessHandler defines a function which is executed for a valid token. | `c.Next()` | | ErrorHandler | `func(fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired PASETO` | | 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` | | SymmetricKey | `[]byte` | Secret key to encrypt token. If present the middleware will generate local tokens. | `nil` | | PrivateKey | `ed25519.PrivateKey` | Secret key to sign the tokens. If present (along with its `PublicKey`) the middleware will generate public tokens. | `nil` | | PublicKey | `crypto.PublicKey` | Public key to verify the tokens. If present (along with `PrivateKey`) the middleware will generate public tokens. | `nil` | | Extractor | `Extractor` | Extractor defines a function to extract the token from the request. | `FromAuthHeader("Bearer")` | ## Available Extractors PASETO middleware uses the shared Fiber extractors (github.com/gofiber/fiber/v3/extractors) and provides several helpers for different token sources: Import them like this: ```go import "github.com/gofiber/fiber/v3/extractors" ``` For an overview and additional examples, see the Fiber Extractors guide: - https://docs.gofiber.io/guide/extractors - `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.** - `extractors.FromHeader(header string)` - Extracts token from the specified HTTP header - `extractors.FromQuery(param string)` - Extracts token from URL query parameters - `extractors.FromParam(param string)` - Extracts token from URL path parameters - `extractors.FromCookie(key string)` - Extracts token from cookies - `extractors.FromForm(param string)` - Extracts token from form data - `extractors.Chain(extrs ...extractors.Extractor)` - Tries multiple extractors in order until one succeeds ### Security Considerations ⚠️ **Security Warning**: When choosing an extractor, consider the security implications: - **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. - **Form-based extractors** (`FromForm`): Similar risks to URL extractors, especially if forms are submitted via GET requests. - **Header-based extractors** (`FromAuthHeader`, `FromHeader`): Most secure as headers are not typically logged or exposed in referrers. - **Cookie-based extractors** (`FromCookie`): Secure for web applications but requires proper cookie security settings (HttpOnly, Secure, SameSite). **Recommendation**: Use `FromAuthHeader("Bearer")` (the default) for production applications unless you have specific requirements that necessitate alternative extractors. ## Migration from TokenPrefix If you were previously using `TokenPrefix`, you can now use `extractors.FromAuthHeader` with the prefix: ```go // Old way pasetoware.New(pasetoware.Config{ SymmetricKey: []byte("secret"), TokenPrefix: "Bearer", }) // New way pasetoware.New(pasetoware.Config{ SymmetricKey: []byte("secret"), Extractor: extractors.FromAuthHeader("Bearer"), }) ``` ## Examples Below have a list of some examples that can help you start to use this middleware. In case of any additional example that doesn't show here, please take a look at the test file. ### SymmetricKey ```go package main import ( "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" pasetoware "github.com/gofiber/contrib/v3/paseto" ) const secretSymmetricKey = "symmetric-secret-key (size = 32)" func main() { app := fiber.New() // Login route app.Post("/login", login) // Unauthenticated route app.Get("/", accessible) // Paseto Middleware with local (encrypted) token apiGroup := app.Group("api", pasetoware.New(pasetoware.Config{ SymmetricKey: []byte(secretSymmetricKey), Extractor: extractors.FromAuthHeader("Bearer"), })) // Restricted Routes apiGroup.Get("/restricted", restricted) err := app.Listen(":8088") if err != nil { return } } func login(c fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") // Throws Unauthorized error if user != "john" || pass != "doe" { return c.SendStatus(fiber.StatusUnauthorized) } // Create token and encrypt it encryptedToken, err := pasetoware.CreateToken([]byte(secretSymmetricKey), user, 12*time.Hour, pasetoware.PurposeLocal) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(fiber.Map{"token": encryptedToken}) } func accessible(c fiber.Ctx) error { return c.SendString("Accessible") } func restricted(c fiber.Ctx) error { payload := pasetoware.FromContext(c).(string) return c.SendString("Welcome " + payload) } ``` #### Test it _Login using username and password to retrieve a token._ ```sh curl --data "user=john&pass=doe" http://localhost:8088/login ``` _Response_ ```json { "token": "" } ``` _Request a restricted resource using the token in Authorization request header._ ```sh curl localhost:8088/api/restricted -H "Authorization: Bearer " ``` _Response_ ```text Welcome john ``` ### SymmetricKey + Custom Validator callback ```go package main import ( "encoding/json" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" "github.com/o1egl/paseto" pasetoware "github.com/gofiber/contrib/v3/paseto" ) const secretSymmetricKey = "symmetric-secret-key (size = 32)" type customPayloadStruct struct { Name string `json:"name"` ExpiresAt time.Time `json:"expiresAt"` } func main() { app := fiber.New() // Login route app.Post("/login", login) // Unauthenticated route app.Get("/", accessible) // Paseto Middleware with local (encrypted) token apiGroup := app.Group("api", pasetoware.New(pasetoware.Config{ SymmetricKey: []byte(secretSymmetricKey), Extractor: extractors.FromAuthHeader("Bearer"), Validate: func(decrypted []byte) (any, error) { var payload customPayloadStruct err := json.Unmarshal(decrypted, &payload) return payload, err }, })) // Restricted Routes apiGroup.Get("/restricted", restricted) err := app.Listen(":8088") if err != nil { return } } func login(c fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") // Throws Unauthorized error if user != "john" || pass != "doe" { return c.SendStatus(fiber.StatusUnauthorized) } // Create the payload payload := customPayloadStruct{ Name: "John Doe", ExpiresAt: time.Now().Add(12 * time.Hour), } // Create token and encrypt it encryptedToken, err := paseto.NewV2().Encrypt([]byte(secretSymmetricKey), payload, nil) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(fiber.Map{"token": encryptedToken}) } func accessible(c fiber.Ctx) error { return c.SendString("Accessible") } func restricted(c fiber.Ctx) error { payload := pasetoware.FromContext(c).(customPayloadStruct) return c.SendString("Welcome " + payload.Name) } ``` ### Cookie Extractor Example ```go package main import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" pasetoware "github.com/gofiber/contrib/v3/paseto" ) const secretSymmetricKey = "symmetric-secret-key (size = 32)" func main() { app := fiber.New() // Paseto Middleware with cookie extractor app.Use(pasetoware.New(pasetoware.Config{ SymmetricKey: []byte(secretSymmetricKey), Extractor: extractors.FromCookie("token"), })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("Protected route") }) app.Listen(":8080") } ``` ### Query Extractor Example ```go package main import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" pasetoware "github.com/gofiber/contrib/v3/paseto" ) const secretSymmetricKey = "symmetric-secret-key (size = 32)" func main() { app := fiber.New() // Paseto Middleware with query extractor app.Use(pasetoware.New(pasetoware.Config{ SymmetricKey: []byte(secretSymmetricKey), Extractor: extractors.FromQuery("token"), })) app.Get("/protected", func(c fiber.Ctx) error { return c.SendString("Protected route") }) app.Listen(":8080") } ``` ### PublicPrivate Key ```go package main import ( "crypto/ed25519" "encoding/hex" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" pasetoware "github.com/gofiber/contrib/v3/paseto" ) const privateKeySeed = "e9c67fe2433aa4110caf029eba70df2c822cad226b6300ead3dcae443ac3810f" var seed, _ = hex.DecodeString(privateKeySeed) var privateKey = ed25519.NewKeyFromSeed(seed) type customPayloadStruct struct { Name string `json:"name"` ExpiresAt time.Time `json:"expiresAt"` } func main() { app := fiber.New() // Login route app.Post("/login", login) // Unauthenticated route app.Get("/", accessible) // Paseto Middleware with public (signed) token apiGroup := app.Group("api", pasetoware.New(pasetoware.Config{ Extractor: extractors.FromAuthHeader("Bearer"), PrivateKey: privateKey, PublicKey: privateKey.Public(), })) // Restricted Routes apiGroup.Get("/restricted", restricted) err := app.Listen(":8088") if err != nil { return } } func login(c fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") // Throws Unauthorized error if user != "john" || pass != "doe" { return c.SendStatus(fiber.StatusUnauthorized) } // Create token and sign it signedToken, err := pasetoware.CreateToken(privateKey, user, 12*time.Hour, pasetoware.PurposePublic) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON(fiber.Map{"token": signedToken}) } func accessible(c fiber.Ctx) error { return c.SendString("Accessible") } func restricted(c fiber.Ctx) error { payload := pasetoware.FromContext(c).(string) return c.SendString("Welcome " + payload) } ``` #### Get the payload from the context ```go payloadFromCtx := pasetoware.FromContext(c) if payloadFromCtx == nil { // Handle case where token is not in context, e.g. by returning an error return } payload := payloadFromCtx.(string) ``` `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): ```go // From a fiber.Ctx (most common usage) payload := pasetoware.FromContext(c) // From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled) payload := pasetoware.FromContext(c.Context()) ``` #### Test it _Login using username and password to retrieve a token._ ```sh curl --data "user=john&pass=doe" http://localhost:8088/login ``` _Response_ ```json { "token": "" } ``` _Request a restricted resource using the token in Authorization request header._ ```sh curl localhost:8088/api/restricted -H "Authorization: Bearer " ``` _Response_ ```text Welcome John Doe ``` ================================================ FILE: v3/paseto/config.go ================================================ package pasetoware import ( "crypto" "crypto/ed25519" "encoding/json" "errors" "fmt" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" "github.com/o1egl/paseto" "golang.org/x/crypto/chacha20poly1305" ) // Config defines the config for PASETO middleware type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(fiber.Ctx) bool // SuccessHandler defines a function which is executed for a valid token. // Optional. Default: c.Next() SuccessHandler fiber.Handler // ErrorHandler defines a function which is executed for an invalid token. // It may be used to define a custom PASETO error. // Optional. Default: 401 Invalid or expired PASETO ErrorHandler fiber.ErrorHandler // Validate 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 Validate PayloadValidator // SymmetricKey to validate local tokens. // If it's set the middleware will use local tokens // // Required if PrivateKey and PublicKey are not set SymmetricKey []byte // PrivateKey to sign public tokens // // If it's set the middleware will use public tokens // Required if SymmetricKey is not set PrivateKey ed25519.PrivateKey // PublicKey to verify public tokens // // If it's set the middleware will use public tokens // Required if SymmetricKey is not set PublicKey crypto.PublicKey // Extractor defines a function to extract the token from the request. // Optional. Default: FromAuthHeader("Bearer"). Extractor extractors.Extractor } // ConfigDefault is the default config var ConfigDefault = Config{ SuccessHandler: nil, ErrorHandler: nil, Validate: nil, SymmetricKey: nil, Extractor: extractors.FromAuthHeader("Bearer"), } func defaultErrorHandler(c fiber.Ctx, err error) error { // default to badRequest if error is ErrMissingToken or any paseto decryption error errorStatus := fiber.StatusBadRequest if errors.Is(err, ErrDataUnmarshal) || errors.Is(err, ErrExpiredToken) { errorStatus = fiber.StatusUnauthorized } return c.Status(errorStatus).SendString(err.Error()) } func defaultValidateFunc(data []byte) (interface{}, error) { var payload paseto.JSONToken if err := json.Unmarshal(data, &payload); err != nil { return nil, ErrDataUnmarshal } if time.Now().After(payload.Expiration) { return nil, ErrExpiredToken } if err := payload.Validate( paseto.ValidAt(time.Now()), paseto.Subject(pasetoTokenSubject), paseto.ForAudience(pasetoTokenAudience), ); err != nil { return "", err } return payload.Get(pasetoTokenField), nil } // Helper function to set default values func configDefault(authConfigs ...Config) Config { // Return default authConfigs if nothing provided config := ConfigDefault if len(authConfigs) > 0 { // Override default authConfigs config = authConfigs[0] } // Set default values if config.SuccessHandler == nil { config.SuccessHandler = func(c fiber.Ctx) error { return c.Next() } } if config.ErrorHandler == nil { config.ErrorHandler = defaultErrorHandler } if config.Validate == nil { config.Validate = defaultValidateFunc } if config.Extractor.Extract == nil { config.Extractor = extractors.FromAuthHeader("Bearer") } if config.SymmetricKey != nil { if len(config.SymmetricKey) != chacha20poly1305.KeySize { panic( fmt.Sprintf( "Fiber: PASETO middleware requires a symmetric key with size %d", chacha20poly1305.KeySize, ), ) } if config.PublicKey != nil || config.PrivateKey != nil { panic("Fiber: PASETO middleware: can't use PublicKey or PrivateKey with SymmetricKey") } } else if config.PublicKey == nil || config.PrivateKey == nil { panic("Fiber: PASETO middleware: need both PublicKey and PrivateKey") } return config } ================================================ FILE: v3/paseto/config_test.go ================================================ package pasetoware import ( "testing" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" ) func assertRecoveryPanic(t *testing.T) { err := recover() assert.Equal(t, true, err != nil) } func Test_Config_No_SymmetricKey(t *testing.T) { defer assertRecoveryPanic(t) config := configDefault() assert.Equal(t, "", config.SymmetricKey) } func Test_Config_Invalid_SymmetricKey(t *testing.T) { defer assertRecoveryPanic(t) config := configDefault() assert.Equal(t, symmetricKey+symmetricKey, config.SymmetricKey) } func Test_ConfigDefault(t *testing.T) { config := configDefault(Config{ SymmetricKey: []byte(symmetricKey), }) assert.Equal(t, extractors.SourceAuthHeader, config.Extractor.Source) assert.Equal(t, fiber.HeaderAuthorization, config.Extractor.Key) assert.Equal(t, "Bearer", config.Extractor.AuthScheme) assert.Empty(t, config.Extractor.Chain) assert.NotNil(t, config.Validate) } func Test_ConfigCustomLookup(t *testing.T) { config := configDefault(Config{ SymmetricKey: []byte(symmetricKey), Extractor: extractors.FromHeader("Custom-Header"), }) assert.Equal(t, extractors.SourceHeader, config.Extractor.Source) assert.Equal(t, "Custom-Header", config.Extractor.Key) assert.Equal(t, "", config.Extractor.AuthScheme) config = configDefault(Config{ SymmetricKey: []byte(symmetricKey), Extractor: extractors.FromQuery("token"), }) assert.Equal(t, extractors.SourceQuery, config.Extractor.Source) assert.Equal(t, "token", config.Extractor.Key) assert.Equal(t, "", config.Extractor.AuthScheme) } ================================================ FILE: v3/paseto/go.mod ================================================ module github.com/gofiber/contrib/v3/paseto go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/google/uuid v1.6.0 github.com/o1egl/paseto v1.0.0 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.50.0 ) require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/paseto/go.sum ================================================ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOdbQRg5nAHt2jrc5QbV0AGuhDdfQI6gXjiFE= github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/paseto/helpers.go ================================================ package pasetoware import ( "crypto/ed25519" "errors" "time" "github.com/o1egl/paseto" ) type TokenPurpose int const ( PurposeLocal TokenPurpose = iota PurposePublic ) var ( ErrExpiredToken = errors.New("token has expired") ErrMissingToken = errors.New("missing PASETO token") ErrDataUnmarshal = errors.New("can't unmarshal token data to Payload type") pasetoObject = paseto.NewV2() ) // PayloadValidator Function that receives the decrypted payload and returns an interface and an error // that's a result of validation logic type PayloadValidator func(decrypted []byte) (interface{}, error) // PayloadCreator Signature of a function that generates a payload token type PayloadCreator func(key []byte, dataInfo string, duration time.Duration, purpose TokenPurpose) (string, error) // Public helper functions // CreateToken Create a new Token Payload that will be stored in PASETO func CreateToken(key []byte, dataInfo string, duration time.Duration, purpose TokenPurpose) (string, error) { payload, err := NewPayload(dataInfo, duration) if err != nil { return "", err } switch purpose { case PurposeLocal: return pasetoObject.Encrypt(key, payload, nil) case PurposePublic: return pasetoObject.Sign(ed25519.PrivateKey(key), payload, nil) default: return pasetoObject.Encrypt(key, payload, nil) } } ================================================ FILE: v3/paseto/paseto.go ================================================ package pasetoware import ( "errors" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" ) // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. type contextKey int // The following contextKey values are defined to store values in context. const ( payloadKey contextKey = iota ) // New PASETO middleware returns a handler that takes a token in the selected lookup param and, // when valid, stores the decrypted payload via fiber.StoreInContext (locals + context when enabled). // See Config for more configuration options. func New(authConfigs ...Config) fiber.Handler { // Set default authConfig config := configDefault(authConfigs...) // Return middleware handler return func(c fiber.Ctx) error { // Filter request to skip middleware if config.Next != nil && config.Next(c) { return c.Next() } token, err := config.Extractor.Extract(c) if err != nil { if errors.Is(err, extractors.ErrNotFound) { return config.ErrorHandler(c, ErrMissingToken) } return config.ErrorHandler(c, err) } var outData []byte if config.SymmetricKey != nil { if err := pasetoObject.Decrypt(token, config.SymmetricKey, &outData, nil); err != nil { return config.ErrorHandler(c, err) } } else { if err := pasetoObject.Verify(token, config.PublicKey, &outData, nil); err != nil { return config.ErrorHandler(c, err) } } payload, err := config.Validate(outData) if err == nil { // Store user information from token into context. fiber.StoreInContext(c, payloadKey, payload) return config.SuccessHandler(c) } return config.ErrorHandler(c, err) } } // FromContext returns the payload from the context. // It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context. func FromContext(ctx any) interface{} { payload, _ := fiber.ValueFromContext[interface{}](ctx, payloadKey) return payload } ================================================ FILE: v3/paseto/paseto_test.go ================================================ package pasetoware import ( "crypto/ed25519" "encoding/hex" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" ) const ( testMessage = "fiber with PASETO middleware!!" invalidToken = "We are gophers!" durationTest = 10 * time.Minute symmetricKey = "go+fiber=love;FiberWithPASETO<3!" privateKeySeed = "e9c67fe2433aa4110caf029eba70df2c822cad226b6300ead3dcae443ac3810f" ) type customPayload struct { Data string `json:"data"` ExpirationTime time.Duration `json:"expiration_time"` CreatedAt time.Time `json:"created_at"` } func createCustomToken(key []byte, dataInfo string, duration time.Duration, purpose TokenPurpose) (string, error) { if purpose == PurposeLocal { return pasetoObject.Encrypt(key, customPayload{ Data: dataInfo, ExpirationTime: duration, CreatedAt: time.Now(), }, nil) } return pasetoObject.Sign(ed25519.PrivateKey(key), customPayload{ Data: dataInfo, ExpirationTime: duration, CreatedAt: time.Now(), }, nil) } func generateTokenRequest( targetRoute string, tokenGenerator PayloadCreator, duration time.Duration, purpose TokenPurpose, schemes ...string, ) (*http.Request, error) { var token string var err error if purpose == PurposeLocal { token, err = tokenGenerator([]byte(symmetricKey), testMessage, duration, purpose) } else { seed, _ := hex.DecodeString(privateKeySeed) privateKey := ed25519.NewKeyFromSeed(seed) token, err = tokenGenerator(privateKey, testMessage, duration, purpose) } if err != nil { return nil, err } request := httptest.NewRequest("GET", targetRoute, nil) scheme := "Bearer" if len(schemes) > 0 && schemes[0] != "" { scheme = schemes[0] } request.Header.Set(fiber.HeaderAuthorization, scheme+" "+token) return request, nil } func getPrivateKey() ed25519.PrivateKey { seed, _ := hex.DecodeString(privateKeySeed) return ed25519.NewKeyFromSeed(seed) } func assertErrorHandler(t *testing.T, toAssert error) fiber.ErrorHandler { t.Helper() return func(ctx fiber.Ctx, err error) error { assert.Equal(t, toAssert, err) assert.Equal(t, true, errors.Is(err, toAssert)) return defaultErrorHandler(ctx, err) } } func Test_PASETO_LocalToken_MissingToken(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), ErrorHandler: assertErrorHandler(t, ErrMissingToken), })) request := httptest.NewRequest("GET", "/", nil) resp, err := app.Test(request) if err == nil { assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } } func Test_PASETO_PublicToken_MissingToken(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), ErrorHandler: assertErrorHandler(t, ErrMissingToken), })) request := httptest.NewRequest("GET", "/", nil) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } func Test_PASETO_LocalToken_ErrDataUnmarshal(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), ErrorHandler: assertErrorHandler(t, ErrDataUnmarshal), })) request, err := generateTokenRequest("/", createCustomToken, durationTest, PurposeLocal) if err == nil { var resp *http.Response resp, err = app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode) } } func Test_PASETO_PublicToken_ErrDataUnmarshal(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), })) request, err := generateTokenRequest("/", createCustomToken, durationTest, PurposePublic) assert.Equal(t, nil, err) var resp *http.Response resp, err = app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode) } func Test_PASETO_LocalToken_ErrTokenExpired(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), ErrorHandler: assertErrorHandler(t, ErrExpiredToken), })) request, err := generateTokenRequest("/", CreateToken, time.Nanosecond*-10, PurposeLocal) if err == nil { var resp *http.Response resp, err = app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode) } } func Test_PASETO_PublicToken_ErrTokenExpired(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), ErrorHandler: assertErrorHandler(t, ErrExpiredToken), })) request, err := generateTokenRequest("/", CreateToken, time.Nanosecond*-10, PurposePublic) assert.Equal(t, nil, err) var resp *http.Response resp, err = app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusUnauthorized, resp.StatusCode) } func Test_PASETO_LocalToken_Next(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), Next: func(_ fiber.Ctx) bool { return true }, })) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func Test_PASETO_PublicToken_Next(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), Next: func(_ fiber.Ctx) bool { return true }, })) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func Test_PASETO_LocalTokenDecrypt(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), })) app.Get("/", func(ctx fiber.Ctx) error { assert.Equal(t, testMessage, FromContext(ctx)) return nil }) request, err := generateTokenRequest("/", CreateToken, durationTest, PurposeLocal) if err == nil { var resp *http.Response resp, err = app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } } func Test_PASETO_PublicTokenVerify(t *testing.T) { seed, _ := hex.DecodeString(privateKeySeed) privateKey := ed25519.NewKeyFromSeed(seed) app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), })) app.Get("/", func(ctx fiber.Ctx) error { assert.Equal(t, testMessage, FromContext(ctx)) return nil }) request, err := generateTokenRequest("/", CreateToken, durationTest, PurposePublic) assert.Equal(t, nil, err) var resp *http.Response resp, err = app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } func Test_PASETO_LocalToken_IncorrectBearerToken(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), Extractor: extractors.FromAuthHeader("Gopher"), })) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } func Test_PASETO_PublicToken_IncorrectBearerToken(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), Extractor: extractors.FromAuthHeader("Gopher"), })) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } func Test_PASETO_LocalToken_InvalidToken(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), })) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } func Test_PASETO_PublicToken_InvalidToken(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), })) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) } func Test_PASETO_LocalToken_CustomValidate(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), Validate: func(data []byte) (interface{}, error) { var payload customPayload if err := json.Unmarshal(data, &payload); err != nil { return nil, ErrDataUnmarshal } if time.Now().After(payload.CreatedAt.Add(payload.ExpirationTime)) { return nil, ErrExpiredToken } return payload.Data, nil }, })) app.Get("/", func(ctx fiber.Ctx) error { assert.Equal(t, testMessage, FromContext(ctx)) return nil }) token, _ := pasetoObject.Encrypt([]byte(symmetricKey), customPayload{ Data: testMessage, ExpirationTime: 10 * time.Minute, CreatedAt: time.Now(), }, nil) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+token) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } func Test_PASETO_PublicToken_CustomValidate(t *testing.T) { privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, PublicKey: privateKey.Public(), Validate: func(data []byte) (interface{}, error) { var payload customPayload if err := json.Unmarshal(data, &payload); err != nil { return nil, ErrDataUnmarshal } if time.Now().After(payload.CreatedAt.Add(payload.ExpirationTime)) { return nil, ErrExpiredToken } return payload.Data, nil }, })) app.Get("/", func(ctx fiber.Ctx) error { assert.Equal(t, testMessage, FromContext(ctx)) return nil }) token, _ := pasetoObject.Sign(privateKey, customPayload{ Data: testMessage, ExpirationTime: 10 * time.Minute, CreatedAt: time.Now(), }, nil) request := httptest.NewRequest("GET", "/", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+token) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } func Test_PASETO_CustomErrorHandler(t *testing.T) { app := fiber.New() customErrorCalled := false app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), ErrorHandler: func(ctx fiber.Ctx, err error) error { customErrorCalled = true return ctx.Status(fiber.StatusTeapot).SendString("Custom PASETO Error: " + err.Error()) }, })) app.Get("/protected", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) request := httptest.NewRequest("GET", "/protected", nil) request.Header.Set(fiber.HeaderAuthorization, "Bearer "+invalidToken) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusTeapot, resp.StatusCode) assert.True(t, customErrorCalled) } func Test_PASETO_CustomSuccessHandler(t *testing.T) { app := fiber.New() customSuccessCalled := false app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), SuccessHandler: func(ctx fiber.Ctx) error { customSuccessCalled = true ctx.Locals("custom", "paseto-success") return ctx.Next() }, })) app.Get("/protected", func(ctx fiber.Ctx) error { if ctx.Locals("custom") == "paseto-success" { return ctx.SendString("Custom Success Handler Worked") } return ctx.SendString("OK") }) request, err := generateTokenRequest("/protected", CreateToken, durationTest, PurposeLocal) assert.Equal(t, nil, err) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) assert.True(t, customSuccessCalled) } func Test_PASETO_InvalidSymmetricKey(t *testing.T) { defer func() { err := recover() assert.NotNil(t, err) }() app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte("invalid-key-length"), // Wrong length })) app.Get("/protected", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) } func Test_PASETO_MissingPublicKey(t *testing.T) { defer func() { err := recover() assert.NotNil(t, err) }() privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ PrivateKey: privateKey, // Missing PublicKey })) app.Get("/protected", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) } func Test_PASETO_MissingPrivateKey(t *testing.T) { defer func() { err := recover() assert.NotNil(t, err) }() app := fiber.New() app.Use(New(Config{ PublicKey: getPrivateKey().Public(), // Missing PrivateKey })) app.Get("/protected", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) } func Test_PASETO_BothKeysProvided(t *testing.T) { defer func() { err := recover() assert.NotNil(t, err) }() privateKey := getPrivateKey() app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), PrivateKey: privateKey, PublicKey: privateKey.Public(), })) app.Get("/protected", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) } func Test_PASETO_FromContextWithoutToken(t *testing.T) { app := fiber.New() app.Get("/no-token", func(ctx fiber.Ctx) error { payload := FromContext(ctx) if payload == nil { return ctx.SendString("No payload as expected") } return ctx.SendString("Unexpected payload") }) request := httptest.NewRequest("GET", "/no-token", nil) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } func Test_PASETO_CustomValidateError(t *testing.T) { app := fiber.New() app.Use(New(Config{ SymmetricKey: []byte(symmetricKey), Validate: func(data []byte) (interface{}, error) { return nil, fiber.NewError(fiber.StatusForbidden, "Custom validation failed") }, ErrorHandler: func(ctx fiber.Ctx, err error) error { return ctx.Status(fiber.StatusForbidden).SendString("Validation failed") }, })) app.Get("/protected", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) request, err := generateTokenRequest("/protected", CreateToken, durationTest, PurposeLocal) assert.Equal(t, nil, err) resp, err := app.Test(request) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusForbidden, resp.StatusCode) } func Test_PASETO_FromContext_PassLocalsToContext(t *testing.T) { app := fiber.New(fiber.Config{PassLocalsToContext: true}) app.Use(New(Config{SymmetricKey: []byte(symmetricKey)})) app.Get("/", func(ctx fiber.Ctx) error { payload := FromContext(ctx) payloadFromContext := FromContext(ctx.Context()) if payload == nil || payloadFromContext == nil { return ctx.SendStatus(fiber.StatusUnauthorized) } return ctx.SendStatus(fiber.StatusOK) }) request, err := generateTokenRequest("/", CreateToken, durationTest, PurposeLocal) assert.NoError(t, err) resp, err := app.Test(request) assert.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) } ================================================ FILE: v3/paseto/payload.go ================================================ package pasetoware import ( "time" "github.com/google/uuid" "github.com/o1egl/paseto" ) const ( pasetoTokenAudience = "gofiber.gophers" pasetoTokenSubject = "user-token" pasetoTokenField = "data" ) // NewPayload generates a new paseto.JSONToken and returns it and a error that can be caused by uuid func NewPayload(userToken string, duration time.Duration) (*paseto.JSONToken, error) { tokenID, err := uuid.NewRandom() if err != nil { return nil, err } timeNow := time.Now() payload := &paseto.JSONToken{ Audience: pasetoTokenAudience, Jti: tokenID.String(), Subject: pasetoTokenSubject, IssuedAt: timeNow, Expiration: timeNow.Add(duration), NotBefore: timeNow, } payload.Set(pasetoTokenField, userToken) return payload, nil } ================================================ FILE: v3/sentry/README.md ================================================ --- id: sentry --- # Sentry ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*sentry*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20sentry/badge.svg) [Sentry](https://sentry.io/) support for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/sentry go get -u github.com/getsentry/sentry-go ``` ## Signature ```go fiberSentry.New(config ...fiberSentry.Config) fiber.Handler fiberSentry.GetHubFromContext(ctx any) *sdk.Hub // sdk "github.com/getsentry/sentry-go" fiberSentry.MustGetHubFromContext(ctx any) *sdk.Hub // sdk "github.com/getsentry/sentry-go" ``` `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`. ## Config | Property | Type | Description | Default | | :-------------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------- | | 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` | | 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` | | Timeout | `time.Duration` | Timeout for the event delivery requests. | `time.Second * 2` | ## Usage `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. You can access it by using the `sentry.GetHubFromContext()` or `sentry.MustGetHubFromContext()` method on the context itself in any of your proceeding middleware and routes. Keep 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. - **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.** ```go package main import ( "fmt" "log" sdk "github.com/getsentry/sentry-go" fiberSentry "github.com/gofiber/contrib/v3/sentry" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/utils" ) func main() { _ = sdk.Init(sdk.ClientOptions{ Dsn: "", BeforeSend: func(event *sdk.Event, hint *sdk.EventHint) *sdk.Event { if hint.Context != nil { if c, ok := hint.Context.Value(sdk.RequestContextKey).(fiber.Ctx); ok { // You have access to the original Context if it panicked fmt.Println(utils.ImmutableString(c.Hostname())) } } fmt.Println(event) return event }, Debug: true, AttachStacktrace: true, }) app := fiber.New() app.Use(fiberSentry.New(fiberSentry.Config{ Repanic: true, WaitForDelivery: true, })) enhanceSentryEvent := func(c fiber.Ctx) error { if hub := fiberSentry.GetHubFromContext(c); hub != nil { hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt") } return c.Next() } app.All("/foo", enhanceSentryEvent, func(c fiber.Ctx) error { panic("y tho") }) app.All("/", func(c fiber.Ctx) error { if hub := fiberSentry.GetHubFromContext(c); hub != nil { hub.WithScope(func(scope *sdk.Scope) { scope.SetExtra("unwantedQuery", "someQueryDataMaybe") hub.CaptureMessage("User provided unwanted query string, but we recovered just fine") }) } return c.SendStatus(fiber.StatusOK) }) log.Fatal(app.Listen(":3000")) } ``` ## Accessing Context in `BeforeSend` callback ```go import ( "fmt" "github.com/gofiber/fiber/v3" sdk "github.com/getsentry/sentry-go" ) sdk.Init(sdk.ClientOptions{ Dsn: "your-public-dsn", BeforeSend: func(event *sdk.Event, hint *sdk.EventHint) *sdk.Event { if hint.Context != nil { if c, ok := hint.Context.Value(sdk.RequestContextKey).(fiber.Ctx); ok { // You have access to the original Context if it panicked fmt.Println(c.Hostname()) } } return event }, }) ``` ## Retrieving the hub with PassLocalsToContext When `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: ```go // From a fiber.Ctx (most common usage) hub := fiberSentry.GetHubFromContext(c) // From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled) hub := fiberSentry.GetHubFromContext(c.Context()) ``` `MustGetHubFromContext` panics if the hub is not found (e.g. in middleware that runs before `sentry`): ```go hub := fiberSentry.MustGetHubFromContext(c) ``` ================================================ FILE: v3/sentry/config.go ================================================ package sentry import "time" // The contextKey type is unexported to prevent collisions with context keys defined in // other packages. type contextKey int const ( hubKey contextKey = iota ) // Config defines the config for middleware. type Config struct { // Repanic configures whether Sentry should repanic after recovery. // Set to true, if Recover middleware is used. // https://github.com/gofiber/fiber/tree/master/middleware/recover // Optional. Default: false Repanic bool // WaitForDelivery configures whether you want to block the request before moving forward with the response. // If Recover middleware is used, it's safe to either skip this option or set it to false. // https://github.com/gofiber/fiber/tree/master/middleware/recover // Optional. Default: false WaitForDelivery bool // Timeout for the event delivery requests. // Optional. Default: 2 Seconds Timeout time.Duration } // ConfigDefault is the default config var ConfigDefault = Config{ Repanic: false, WaitForDelivery: false, Timeout: time.Second * 2, } // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided if len(config) < 1 { return ConfigDefault } // Override default config cfg := config[0] if cfg.Timeout == 0 { cfg.Timeout = time.Second * 2 } return cfg } ================================================ FILE: v3/sentry/go.mod ================================================ module github.com/gofiber/contrib/v3/sentry go 1.25.0 require ( github.com/getsentry/sentry-go v0.45.1 github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/stretchr/testify v1.11.1 github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect ) ================================================ FILE: v3/sentry/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getsentry/sentry-go v0.45.1 h1:9rfzJtGiJG+MGIaWZXidDGHcH5GU1Z5y0WVJGf9nysw= github.com/getsentry/sentry-go v0.45.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/sentry/sentry.go ================================================ package sentry import ( "context" "github.com/getsentry/sentry-go" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" "github.com/gofiber/utils/v2" ) // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) // Return new handler return func(c fiber.Ctx) error { // Convert fiber request to http request r, err := adaptor.ConvertRequest(c, true) if err != nil { return err } // Init sentry hub hub := sentry.CurrentHub().Clone() scope := hub.Scope() scope.SetRequest(r) scope.SetRequestBody(utils.CopyBytes(c.Body())) fiber.StoreInContext(c, hubKey, hub) // Catch panics defer func() { if err := recover(); err != nil { eventID := hub.RecoverWithContext( context.WithValue(context.Background(), sentry.RequestContextKey, c), err, ) if eventID != nil && cfg.WaitForDelivery { hub.Flush(cfg.Timeout) } if cfg.Repanic { panic(err) } } }() // Return err if exist, else move to next handler return c.Next() } } // MustGetHubFromContext returns the Sentry hub from context. // It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context. // Panics if the hub is not found or has an unexpected type. func MustGetHubFromContext(ctx any) *sentry.Hub { hub := GetHubFromContext(ctx) if hub == nil { panic("sentry: hub not found in context or has unexpected type") } return hub } // GetHubFromContext returns the Sentry hub from context. // It accepts fiber.CustomCtx, fiber.Ctx, *fasthttp.RequestCtx, and context.Context. func GetHubFromContext(ctx any) *sentry.Hub { hub, ok := fiber.ValueFromContext[*sentry.Hub](ctx, hubKey) if !ok { return nil } return hub } ================================================ FILE: v3/sentry/sentry_test.go ================================================ package sentry import ( "fmt" "net/http" "strings" "testing" "time" "github.com/getsentry/sentry-go" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" ) type testCase struct { desc string path string method string body string handler fiber.Handler event *sentry.Event } func testCasesBeforeRegister(t *testing.T) []testCase { return []testCase{ { desc: "MustGetHubFromContext without Sentry middleware", path: "/no-middleware", method: "GET", handler: func(c fiber.Ctx) error { defer func() { if r := recover(); r == nil { t.Fatal("MustGetHubFromContext did not panic") } }() _ = MustGetHubFromContext(c) // This should panic return nil }, event: nil, // No event expected because a panic should occur }, { desc: "GetHubFromContext without Sentry middleware", path: "/no-middleware-2", method: "GET", handler: func(c fiber.Ctx) error { hub := GetHubFromContext(c) if hub != nil { t.Fatal("Expected nil, got a Sentry hub instance") } return nil }, event: nil, // No Sentry event expected here }, } } var testCasesAfterRegister = []testCase{ { desc: "panic", path: "/panic", method: "GET", handler: func(c fiber.Ctx) error { panic("test") }, event: &sentry.Event{ Level: sentry.LevelFatal, Message: "test", Request: &sentry.Request{ URL: "http://example.com/panic", Method: "GET", Headers: map[string]string{ "Host": "example.com", "User-Agent": "fiber", }, }, }, }, { desc: "post", path: "/post", method: "POST", body: "payload", handler: func(c fiber.Ctx) error { hub := MustGetHubFromContext(c) hub.CaptureMessage("post: " + string(c.Body())) return nil }, event: &sentry.Event{ Level: sentry.LevelInfo, Message: "post: payload", Request: &sentry.Request{ URL: "http://example.com/post", Method: "POST", Data: "payload", Headers: map[string]string{ "Content-Length": "7", "Host": "example.com", "User-Agent": "fiber", }, }, }, }, { desc: "get", path: "/get", method: "GET", handler: func(c fiber.Ctx) error { hub := MustGetHubFromContext(c) hub.CaptureMessage("get") return nil }, event: &sentry.Event{ Level: sentry.LevelInfo, Message: "get", Request: &sentry.Request{ URL: "http://example.com/get", Method: "GET", Headers: map[string]string{ "Host": "example.com", "User-Agent": "fiber", }, }, }, }, { desc: "large body", path: "/post/large", method: "POST", body: strings.Repeat("Large", 3*1024), // 15 KB handler: func(c fiber.Ctx) error { hub := MustGetHubFromContext(c) hub.CaptureMessage(fmt.Sprintf("post: %d KB", len(c.Body())/1024)) return nil }, event: &sentry.Event{ Level: sentry.LevelInfo, Message: "post: 15 KB", Request: &sentry.Request{ URL: "http://example.com/post/large", Method: "POST", // Actual request body omitted because too large. Data: "", Headers: map[string]string{ "Content-Length": "15360", "Host": "example.com", "User-Agent": "fiber", }, }, }, }, { desc: "ignore body", path: "/post/body-ignored", method: "POST", body: "client sends, fasthttp always reads, SDK reports", handler: func(c fiber.Ctx) error { hub := MustGetHubFromContext(c) hub.CaptureMessage("body ignored") return nil }, event: &sentry.Event{ Level: sentry.LevelInfo, Message: "body ignored", Request: &sentry.Request{ URL: "http://example.com/post/body-ignored", Method: "POST", // Actual request body included because fasthttp always // reads full request body. Data: "client sends, fasthttp always reads, SDK reports", Headers: map[string]string{ "Content-Length": "48", "Host": "example.com", "User-Agent": "fiber", }, }, }, }, } func Test_Sentry(t *testing.T) { app := fiber.New() testFunc := func(t *testing.T, tC testCase) { t.Run(tC.desc, func(t *testing.T) { if err := sentry.Init(sentry.ClientOptions{ BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { require.Equal(t, tC.event.Message, event.Message) require.Equal(t, tC.event.Request, event.Request) require.Equal(t, tC.event.Level, event.Level) require.Equal(t, tC.event.Exception, event.Exception) return event }, }); err != nil { t.Fatal(err) } app.Add([]string{tC.method}, tC.path, tC.handler) req, err := http.NewRequest(tC.method, "http://example.com"+tC.path, strings.NewReader(tC.body)) if err != nil { t.Fatal(err) } req.Header.Set("User-Agent", "fiber") resp, err := app.Test(req) if err != nil { t.Fatalf("Request %q failed: %s", tC.path, err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Status code = %d", resp.StatusCode) } }) } for _, tC := range testCasesBeforeRegister(t) { testFunc(t, tC) } app.Use(New()) for _, tC := range testCasesAfterRegister { testFunc(t, tC) } if ok := sentry.Flush(time.Second); !ok { t.Fatal("sentry.Flush timed out") } } func Test_GetHubFromContext_PassLocalsToContext(t *testing.T) { app := fiber.New(fiber.Config{PassLocalsToContext: true}) app.Use(New()) app.Get("/", func(c fiber.Ctx) error { hub := GetHubFromContext(c) hubFromContext := GetHubFromContext(c.Context()) require.NotNil(t, hub) require.NotNil(t, hubFromContext) return c.SendStatus(http.StatusOK) }) req, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) require.NoError(t, err) resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) } ================================================ FILE: v3/socketio/README.md ================================================ --- id: socketio --- # Socket.io ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*socketio*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20Socket.io/badge.svg) WebSocket wrapper for [Fiber](https://github.com/gofiber/fiber) with events support and inspired by [Socket.io](https://github.com/socketio/socket.io) **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/socketio ``` ## Signatures ```go // Initialize new socketio in the callback this will // execute a callback that expects kws *Websocket Object // and optional config websocket.Config func New(callback func(kws *Websocket), config ...websocket.Config) func(fiber.Ctx) error ``` ```go // Add listener callback for an event into the listeners list func On(event string, callback func(payload *EventPayload)) ``` ```go // Emit the message to a specific socket uuids list // Ignores all errors func EmitToList(uuids []string, message []byte) ``` ```go // Emit to a specific socket connection func EmitTo(uuid string, message []byte) error ``` ```go // Broadcast to all the active connections // except avoid broadcasting the message to itself func Broadcast(message []byte) ``` ```go // Fire custom event on all connections func Fire(event string, data []byte) ``` ## Example ```go package main import ( "encoding/json" "fmt" "log" "github.com/gofiber/contrib/v3/socketio" "github.com/gofiber/contrib/v3/websocket" "github.com/gofiber/fiber/v3" ) // MessageObject Basic chat message object type MessageObject struct { Data string `json:"data"` From string `json:"from"` Event string `json:"event"` To string `json:"to"` } func main() { // The key for the map is message.to clients := make(map[string]string) // Start a new Fiber application app := fiber.New() // Setup the middleware to retrieve the data sent in first GET request app.Use(func(c fiber.Ctx) error { // IsWebSocketUpgrade returns true if the client // requested upgrade to the WebSocket protocol. if websocket.IsWebSocketUpgrade(c) { c.Locals("allowed", true) return c.Next() } return fiber.ErrUpgradeRequired }) // Multiple event handling supported socketio.On(socketio.EventConnect, func(ep *socketio.EventPayload) { fmt.Printf("Connection event 1 - User: %s", ep.Kws.GetStringAttribute("user_id")) }) // Custom event handling supported socketio.On("CUSTOM_EVENT", func(ep *socketio.EventPayload) { fmt.Printf("Custom event - User: %s", ep.Kws.GetStringAttribute("user_id")) // ---> // DO YOUR BUSINESS HERE // ---> }) // On message event socketio.On(socketio.EventMessage, func(ep *socketio.EventPayload) { fmt.Printf("Message event - User: %s - Message: %s", ep.Kws.GetStringAttribute("user_id"), string(ep.Data)) message := MessageObject{} // Unmarshal the json message // { // "from": "", // "to": "", // "event": "CUSTOM_EVENT", // "data": "hello" //} err := json.Unmarshal(ep.Data, &message) if err != nil { fmt.Println(err) return } // Fire custom event based on some // business logic if message.Event != "" { ep.Kws.Fire(message.Event, []byte(message.Data)) } // Emit the message directly to specified user err = ep.Kws.EmitTo(clients[message.To], ep.Data, socketio.TextMessage) if err != nil { fmt.Println(err) } }) // On disconnect event socketio.On(socketio.EventDisconnect, func(ep *socketio.EventPayload) { // Remove the user from the local clients delete(clients, ep.Kws.GetStringAttribute("user_id")) fmt.Printf("Disconnection event - User: %s", ep.Kws.GetStringAttribute("user_id")) }) // On close event // This event is called when the server disconnects the user actively with .Close() method socketio.On(socketio.EventClose, func(ep *socketio.EventPayload) { // Remove the user from the local clients delete(clients, ep.Kws.GetStringAttribute("user_id")) fmt.Printf("Close event - User: %s", ep.Kws.GetStringAttribute("user_id")) }) // On error event socketio.On(socketio.EventError, func(ep *socketio.EventPayload) { fmt.Printf("Error event - User: %s", ep.Kws.GetStringAttribute("user_id")) }) app.Get("/ws/:id", socketio.New(func(kws *socketio.Websocket) { // Retrieve the user id from endpoint userId := kws.Params("id") // Add the connection to the list of the connected clients // The UUID is generated randomly and is the key that allow // socketio to manage Emit/EmitTo/Broadcast clients[userId] = kws.UUID // Every websocket connection has an optional session key => value storage kws.SetAttribute("user_id", userId) //Broadcast to all the connected users the newcomer kws.Broadcast([]byte(fmt.Sprintf("New user connected: %s and UUID: %s", userId, kws.UUID)), true, socketio.TextMessage) //Write welcome message kws.Emit([]byte(fmt.Sprintf("Hello user: %s with UUID: %s", userId, kws.UUID)), socketio.TextMessage) })) log.Fatal(app.Listen(":3000")) } ``` --- ## Supported events | Const | Event | Description | |:----------------|:-------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | EventMessage | `message` | Fired when a Text/Binary message is received | | 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) | | EventPong | `pong` | Refer to ping description | | EventDisconnect | `disconnect` | Fired on disconnection. The error provided in disconnection event as defined in RFC 6455, section 11.7. | | EventConnect | `connect` | Fired on first connection | | EventClose | `close` | Fired when the connection is actively closed from the server. Different from client disconnection | | EventError | `error` | Fired when some error appears useful also for debugging websockets | ## Event Payload object | Variable | Type | Description | |:-----------------|:--------------------|:--------------------------------------------------------------------------------| | Kws | `*Websocket` | The connection object | | Name | `string` | The name of the event | | SocketUUID | `string` | Unique connection UUID | | SocketAttributes | `map[string]string` | Optional websocket attributes | | Error | `error` | (optional) Fired from disconnection or error events | | Data | `[]byte` | Data used on Message and on Error event, contains the payload for custom events | ## Socket instance functions | Name | Type | Description | |:-------------|:---------|:----------------------------------------------------------------------------------| | SetAttribute | `void` | Set a specific attribute for the specific socket connection | | GetUUID | `string` | Get socket connection UUID | | SetUUID | `error` | Set socket connection UUID | | GetAttribute | `string` | Get a specific attribute from the socket attributes | | EmitToList | `void` | Emit the message to a specific socket uuids list | | EmitTo | `error` | Emit to a specific socket connection | | Broadcast | `void` | Broadcast to all the active connections except broadcasting the message to itself | | Fire | `void` | Fire custom event | | Emit | `void` | Emit/Write the message into the given connection | | Close | `void` | Actively close the connection from the server | **Note: the FastHTTP connection can be accessed directly from the instance** ```go kws.Conn ``` ================================================ FILE: v3/socketio/go.mod ================================================ module github.com/gofiber/contrib/v3/socketio go 1.25.0 require ( github.com/fasthttp/websocket v1.5.12 github.com/gofiber/contrib/v3/websocket v1.0.0 github.com/gofiber/fiber/v3 v3.1.0 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 github.com/valyala/fasthttp v1.70.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/gofiber/contrib/v3/websocket => ../websocket ================================================ FILE: v3/socketio/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE= github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/socketio/socketio.go ================================================ package socketio import ( "context" "errors" "sync" "time" "github.com/gofiber/contrib/v3/websocket" "github.com/gofiber/fiber/v3" "github.com/google/uuid" ) // Source @url:https://github.com/gorilla/websocket/blob/master/conn.go#L61 // The message types are defined in RFC 6455, section 11.8. const ( // TextMessage denotes a text data message. The text message payload is // interpreted as UTF-8 encoded text data. TextMessage = 1 // BinaryMessage denotes a binary data message. BinaryMessage = 2 // CloseMessage denotes a close control message. The optional message // payload contains a numeric code and text. Use the FormatCloseMessage // function to format a close message payload. CloseMessage = 8 // PingMessage denotes a ping control message. The optional message payload // is UTF-8 encoded text. PingMessage = 9 // PongMessage denotes a pong control message. The optional message payload // is UTF-8 encoded text. PongMessage = 10 ) // Supported event list const ( // EventMessage Fired when a Text/Binary message is received EventMessage = "message" // EventPing More details here: // @url https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets EventPing = "ping" EventPong = "pong" // EventDisconnect Fired on disconnection // The error provided in disconnection event // is defined in RFC 6455, section 11.7. // @url https://github.com/gofiber/websocket/blob/cd4720c435de415b864d975a9ca23a47eaf081ef/websocket.go#L192 EventDisconnect = "disconnect" // EventConnect Fired on first connection EventConnect = "connect" // EventClose Fired when the connection is actively closed from the server EventClose = "close" // EventError Fired when some error appears useful also for debugging websockets EventError = "error" ) var ( // ErrorInvalidConnection The addressed Conn connection is not available anymore // error data is the uuid of that connection ErrorInvalidConnection = errors.New("message cannot be delivered invalid/gone connection") // ErrorUUIDDuplication The UUID already exists in the pool ErrorUUIDDuplication = errors.New("UUID already exists in the available connections pool") ) var ( PongTimeout = 1 * time.Second // RetrySendTimeout retry after 20 ms if there is an error RetrySendTimeout = 20 * time.Millisecond //MaxSendRetry define max retries if there are socket issues MaxSendRetry = 5 // ReadTimeout Instead of reading in a for loop, try to avoid full CPU load taking some pause ReadTimeout = 10 * time.Millisecond ) // Raw form of websocket message type message struct { // Message type mType int // Message data data []byte // Message send retries when error retries int } // EventPayload Event Payload is the object that // stores all the information about the event and // the connection type EventPayload struct { // The connection object Kws *Websocket // The name of the event Name string // Unique connection UUID SocketUUID string // Optional websocket attributes SocketAttributes map[string]any // Optional error when are fired events like // - Disconnect // - Error Error error // Data is used on Message and on Error event Data []byte } type ws interface { IsAlive() bool GetUUID() string SetUUID(uuid string) error SetAttribute(key string, attribute interface{}) GetAttribute(key string) interface{} GetIntAttribute(key string) int GetStringAttribute(key string) string EmitToList(uuids []string, message []byte, mType ...int) EmitTo(uuid string, message []byte, mType ...int) error Broadcast(message []byte, except bool, mType ...int) Fire(event string, data []byte) Emit(message []byte, mType ...int) Close() pong(ctx context.Context) write(messageType int, messageBytes []byte) run() read(ctx context.Context) disconnected(err error) createUUID() string randomUUID() string fireEvent(event string, data []byte, error error) } type Websocket struct { once sync.Once mu sync.RWMutex // The Fiber.Websocket connection Conn *websocket.Conn // Define if the connection is alive or not isAlive bool // Queue of messages sent from the socket queue chan message // Channel to signal when this websocket is closed // so go routines will stop gracefully done chan struct{} // Attributes map collection for the connection attributes map[string]interface{} // Unique id of the connection UUID string // Wrap Fiber Locals function Locals func(key string) interface{} // Wrap Fiber Params function Params func(key string, defaultValue ...string) string // Wrap Fiber Query function Query func(key string, defaultValue ...string) string // Wrap Fiber Cookies function Cookies func(key string, defaultValue ...string) string } type safePool struct { sync.RWMutex // List of the connections alive conn map[string]ws } // Pool with the active connections var pool = safePool{ conn: make(map[string]ws), } func (p *safePool) set(ws ws) { p.Lock() p.conn[ws.GetUUID()] = ws p.Unlock() } func (p *safePool) all() map[string]ws { p.RLock() ret := make(map[string]ws, 0) for wsUUID, kws := range p.conn { ret[wsUUID] = kws } p.RUnlock() return ret } func (p *safePool) get(key string) (ws, error) { p.RLock() ret, ok := p.conn[key] p.RUnlock() if !ok { return nil, ErrorInvalidConnection } return ret, nil } func (p *safePool) contains(key string) bool { p.RLock() _, ok := p.conn[key] p.RUnlock() return ok } func (p *safePool) delete(key string) { p.Lock() delete(p.conn, key) p.Unlock() } //nolint:all func (p *safePool) reset() { p.Lock() p.conn = make(map[string]ws) p.Unlock() } type safeListeners struct { sync.RWMutex list map[string][]eventCallback } func (l *safeListeners) set(event string, callback eventCallback) { l.Lock() listeners.list[event] = append(listeners.list[event], callback) l.Unlock() } func (l *safeListeners) get(event string) []eventCallback { l.RLock() defer l.RUnlock() if _, ok := l.list[event]; !ok { return make([]eventCallback, 0) } ret := make([]eventCallback, 0) ret = append(ret, l.list[event]...) return ret } // List of the listeners for the events var listeners = safeListeners{ list: make(map[string][]eventCallback), } func New(callback func(kws *Websocket), config ...websocket.Config) func(fiber.Ctx) error { return websocket.New(func(c *websocket.Conn) { kws := &Websocket{ Conn: c, Locals: func(key string) interface{} { return c.Locals(key) }, Params: func(key string, defaultValue ...string) string { return c.Params(key, defaultValue...) }, Query: func(key string, defaultValue ...string) string { return c.Query(key, defaultValue...) }, Cookies: func(key string, defaultValue ...string) string { return c.Cookies(key, defaultValue...) }, queue: make(chan message, 100), done: make(chan struct{}, 1), attributes: make(map[string]interface{}), isAlive: true, } // Generate uuid kws.UUID = kws.createUUID() // register the connection into the pool pool.set(kws) // execute the callback of the socket initialization callback(kws) kws.fireEvent(EventConnect, nil, nil) // Run the loop for the given connection kws.run() }, config...) } func (kws *Websocket) GetUUID() string { kws.mu.RLock() defer kws.mu.RUnlock() return kws.UUID } func (kws *Websocket) SetUUID(uuid string) error { pool.Lock() defer pool.Unlock() kws.mu.Lock() defer kws.mu.Unlock() prevUUID := kws.UUID if prevUUID == uuid { return nil } kws.UUID = uuid if existing, ok := pool.conn[uuid]; ok && existing != kws { kws.UUID = prevUUID return ErrorUUIDDuplication } if prevUUID != "" { delete(pool.conn, prevUUID) } pool.conn[uuid] = kws return nil } // SetAttribute Set a specific attribute for the specific socket connection func (kws *Websocket) SetAttribute(key string, attribute interface{}) { kws.mu.Lock() defer kws.mu.Unlock() kws.attributes[key] = attribute } // GetAttribute Get a specific attribute from the socket attributes func (kws *Websocket) GetAttribute(key string) interface{} { kws.mu.RLock() defer kws.mu.RUnlock() value, ok := kws.attributes[key] if ok { return value } return nil } // GetIntAttribute Convenience method to retrieve an attribute as an int. // Will panic if attribute is not an int. func (kws *Websocket) GetIntAttribute(key string) int { kws.mu.RLock() defer kws.mu.RUnlock() value, ok := kws.attributes[key] if ok { return value.(int) } return 0 } // GetStringAttribute Convenience method to retrieve an attribute as a string. func (kws *Websocket) GetStringAttribute(key string) string { kws.mu.RLock() defer kws.mu.RUnlock() value, ok := kws.attributes[key] if ok { return value.(string) } return "" } // EmitToList Emit the message to a specific socket uuids list func (kws *Websocket) EmitToList(uuids []string, message []byte, mType ...int) { for _, wsUUID := range uuids { err := kws.EmitTo(wsUUID, message, mType...) if err != nil { kws.fireEvent(EventError, message, err) } } } // EmitToList Emit the message to a specific socket uuids list // Ignores all errors func EmitToList(uuids []string, message []byte, mType ...int) { for _, wsUUID := range uuids { _ = EmitTo(wsUUID, message, mType...) } } // EmitTo Emit to a specific socket connection func (kws *Websocket) EmitTo(uuid string, message []byte, mType ...int) error { conn, err := pool.get(uuid) if err != nil { return err } if !pool.contains(uuid) || !conn.IsAlive() { kws.fireEvent(EventError, []byte(uuid), ErrorInvalidConnection) return ErrorInvalidConnection } conn.Emit(message, mType...) return nil } // EmitTo Emit to a specific socket connection func EmitTo(uuid string, message []byte, mType ...int) error { conn, err := pool.get(uuid) if err != nil { return err } if !pool.contains(uuid) || !conn.IsAlive() { return ErrorInvalidConnection } conn.Emit(message, mType...) return nil } // Broadcast to all the active connections // except avoid broadcasting the message to itself func (kws *Websocket) Broadcast(message []byte, except bool, mType ...int) { for wsUUID := range pool.all() { if except && kws.UUID == wsUUID { continue } err := kws.EmitTo(wsUUID, message, mType...) if err != nil { kws.fireEvent(EventError, message, err) } } } // Broadcast to all the active connections func Broadcast(message []byte, mType ...int) { for _, kws := range pool.all() { kws.Emit(message, mType...) } } // Fire custom event func (kws *Websocket) Fire(event string, data []byte) { kws.fireEvent(event, data, nil) } // Fire custom event on all connections func Fire(event string, data []byte) { fireGlobalEvent(event, data, nil) } // Emit /Write the message into the given connection func (kws *Websocket) Emit(message []byte, mType ...int) { t := TextMessage if len(mType) > 0 { t = mType[0] } kws.write(t, message) } // Close Actively close the connection from the server func (kws *Websocket) Close() { kws.write(CloseMessage, []byte("Connection closed")) kws.fireEvent(EventClose, nil, nil) } func (kws *Websocket) IsAlive() bool { kws.mu.RLock() defer kws.mu.RUnlock() return kws.isAlive } func (kws *Websocket) hasConn() bool { kws.mu.RLock() defer kws.mu.RUnlock() return kws.Conn.Conn != nil } func (kws *Websocket) setAlive(alive bool) { kws.mu.Lock() defer kws.mu.Unlock() kws.isAlive = alive } //nolint:all func (kws *Websocket) queueLength() int { kws.mu.RLock() defer kws.mu.RUnlock() return len(kws.queue) } // pong writes a control message to the client func (kws *Websocket) pong(ctx context.Context) { timeoutTicker := time.NewTicker(PongTimeout) defer timeoutTicker.Stop() for { select { case <-timeoutTicker.C: kws.write(PongMessage, []byte{}) case <-ctx.Done(): return } } } // Add in message queue func (kws *Websocket) write(messageType int, messageBytes []byte) { kws.queue <- message{ mType: messageType, data: messageBytes, retries: 0, } } // Send out message queue func (kws *Websocket) send(ctx context.Context) { for { select { case message := <-kws.queue: if !kws.hasConn() { if message.retries <= MaxSendRetry { // retry without blocking the sending thread go func() { time.Sleep(RetrySendTimeout) message.retries = message.retries + 1 kws.queue <- message }() } continue } kws.mu.RLock() err := kws.Conn.WriteMessage(message.mType, message.data) kws.mu.RUnlock() if err != nil { kws.disconnected(err) } case <-ctx.Done(): return } } } // Start Pong/Read/Write functions // // Needs to be blocking, otherwise the connection would close. func (kws *Websocket) run() { ctx, cancelFunc := context.WithCancel(context.Background()) go kws.pong(ctx) go kws.read(ctx) go kws.send(ctx) <-kws.done // block until one event is sent to the done channel cancelFunc() } // Listen for incoming messages // and filter by message type func (kws *Websocket) read(ctx context.Context) { timeoutTicker := time.NewTicker(ReadTimeout) defer timeoutTicker.Stop() for { select { case <-timeoutTicker.C: if !kws.hasConn() { continue } kws.mu.RLock() mType, msg, err := kws.Conn.ReadMessage() kws.mu.RUnlock() if mType == PingMessage { kws.fireEvent(EventPing, nil, nil) continue } if mType == PongMessage { kws.fireEvent(EventPong, nil, nil) continue } if mType == CloseMessage { kws.disconnected(nil) return } if err != nil { kws.disconnected(err) return } // We have a message and we fire the message event kws.fireEvent(EventMessage, msg, nil) case <-ctx.Done(): return } } } // When the connection closes, disconnected method func (kws *Websocket) disconnected(err error) { kws.fireEvent(EventDisconnect, nil, err) // may be called multiple times from different go routines if kws.IsAlive() { kws.once.Do(func() { kws.setAlive(false) close(kws.done) }) } // Fire error event if the connection is // disconnected by an error if err != nil { kws.fireEvent(EventError, nil, err) } // Remove the socket from the pool pool.delete(kws.UUID) } // Create random UUID for each connection func (kws *Websocket) createUUID() string { return kws.randomUUID() } // Generate random UUID. func (kws *Websocket) randomUUID() string { return uuid.New().String() } // Fires event on all connections. func fireGlobalEvent(event string, data []byte, error error) { for _, kws := range pool.all() { kws.fireEvent(event, data, error) } } // Checks if there is at least a listener for a given event // and loop over the callbacks registered func (kws *Websocket) fireEvent(event string, data []byte, error error) { callbacks := listeners.get(event) for _, callback := range callbacks { callback(&EventPayload{ Kws: kws, Name: event, SocketUUID: kws.UUID, SocketAttributes: kws.attributes, Data: data, Error: error, }) } } type eventCallback func(payload *EventPayload) // On Add listener callback for an event into the listeners list func On(event string, callback eventCallback) { listeners.set(event, callback) } ================================================ FILE: v3/socketio/socketio_test.go ================================================ package socketio import ( "context" "net" "strconv" "sync" "testing" "time" "github.com/fasthttp/websocket" fws "github.com/gofiber/contrib/v3/websocket" "github.com/gofiber/fiber/v3" "github.com/google/uuid" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp/fasthttputil" ) const numTestConn = 10 const numParallelTestConn = 5_000 type HandlerMock struct { mock.Mock wg sync.WaitGroup } type WebsocketMock struct { mock.Mock mu sync.RWMutex wg sync.WaitGroup Conn *websocket.Conn isAlive bool queue map[string]message attributes map[string]string UUID string Locals func(key string) interface{} Params func(key string, defaultValue ...string) string Query func(key string, defaultValue ...string) string Cookies func(key string, defaultValue ...string) string } func (s *WebsocketMock) SetUUID(uuid string) error { s.mu.Lock() defer s.mu.Unlock() if pool.contains(uuid) { panic(ErrorUUIDDuplication) } s.UUID = uuid return nil } func (s *WebsocketMock) GetIntAttribute(key string) int { s.mu.RLock() defer s.mu.RUnlock() value, ok := s.attributes[key] if ok { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return 0 } func (s *WebsocketMock) GetStringAttribute(key string) string { s.mu.RLock() defer s.mu.RUnlock() value, ok := s.attributes[key] if ok { return value } return "" } func (h *HandlerMock) OnCustomEvent(payload *EventPayload) { h.Called(payload) h.wg.Done() } func (s *WebsocketMock) Emit(message []byte, _ ...int) { s.Called(message) s.wg.Done() } func (s *WebsocketMock) IsAlive() bool { args := s.Called() return args.Bool(0) } func (s *WebsocketMock) GetUUID() string { return s.UUID } func TestParallelConnections(t *testing.T) { pool.reset() // create test server cfg := fiber.Config{} app := fiber.New(cfg) ln := fasthttputil.NewInmemoryListener() wg := sync.WaitGroup{} defer func() { _ = app.Shutdown() _ = ln.Close() }() // attach upgrade middleware app.Use(upgradeMiddleware) // send back response on correct message On(EventMessage, func(payload *EventPayload) { if string(payload.Data) == "test" { payload.Kws.Emit([]byte("response")) } }) // create websocket endpoint app.Get("/", New(func(kws *Websocket) { })) // start server go func() { _ = app.Listener(ln) }() wsURL := "ws://" + ln.Addr().String() // create concurrent connections for i := 0; i < numParallelTestConn; i++ { wg.Add(1) go func() { dialer := &websocket.Dialer{ NetDial: func(network, addr string) (net.Conn, error) { return ln.Dial() }, HandshakeTimeout: 45 * time.Second, } dial, _, err := dialer.Dial(wsURL, nil) if err != nil { t.Error(err) return } if err := dial.WriteMessage(websocket.TextMessage, []byte("test")); err != nil { t.Error(err) return } tp, m, err := dial.ReadMessage() if err != nil { t.Error(err) return } require.Equal(t, TextMessage, tp) require.Equal(t, "response", string(m)) wg.Done() if err := dial.Close(); err != nil { t.Error(err) return } }() } wg.Wait() } func TestGlobalFire(t *testing.T) { pool.reset() // simulate connections for i := 0; i < numTestConn; i++ { kws := createWS() pool.set(kws) } h := new(HandlerMock) // setup expectations h.On("OnCustomEvent", mock.Anything).Return(nil) // Moved before registration of the event // if after can cause: panic: sync: negative WaitGroup counter h.wg.Add(numTestConn) // register custom event handler On("customevent", h.OnCustomEvent) // fire global custom event on all connections Fire("customevent", []byte("test")) h.wg.Wait() h.AssertNumberOfCalls(t, "OnCustomEvent", numTestConn) } func TestGlobalBroadcast(t *testing.T) { pool.reset() for i := 0; i < numParallelTestConn; i++ { mws := new(WebsocketMock) mws.SetUUID(mws.createUUID()) pool.set(mws) // setup expectations mws.On("Emit", mock.Anything).Return(nil) mws.wg.Add(1) } // send global broadcast to all connections Broadcast([]byte("test"), TextMessage) for _, mws := range pool.all() { mws.(*WebsocketMock).wg.Wait() mws.(*WebsocketMock).AssertNumberOfCalls(t, "Emit", 1) } } func TestGlobalEmitTo(t *testing.T) { pool.reset() aliveUUID := "80a80sdf809dsf" closedUUID := "las3dfj09808" alive := new(WebsocketMock) alive.UUID = aliveUUID pool.set(alive) closed := new(WebsocketMock) closed.UUID = closedUUID pool.set(closed) // setup expectations alive.On("Emit", mock.Anything).Return(nil) alive.On("IsAlive").Return(true) closed.On("IsAlive").Return(false) var err error err = EmitTo("non-existent", []byte("error")) require.Equal(t, ErrorInvalidConnection, err) err = EmitTo(closedUUID, []byte("error")) require.Equal(t, ErrorInvalidConnection, err) alive.wg.Add(1) // send global broadcast to all connections err = EmitTo(aliveUUID, []byte("test")) require.Nil(t, err) alive.wg.Wait() alive.AssertNumberOfCalls(t, "Emit", 1) } func TestGlobalEmitToList(t *testing.T) { pool.reset() uuids := []string{ "80a80sdf809dsf", "las3dfj09808", } for _, id := range uuids { kws := new(WebsocketMock) kws.SetUUID(id) kws.On("Emit", mock.Anything).Return(nil) kws.On("IsAlive").Return(true) kws.wg.Add(1) pool.set(kws) } // send global broadcast to all connections EmitToList(uuids, []byte("test"), TextMessage) for _, kws := range pool.all() { kws.(*WebsocketMock).wg.Wait() kws.(*WebsocketMock).AssertNumberOfCalls(t, "Emit", 1) } } func TestWebsocket_GetIntAttribute(t *testing.T) { kws := &Websocket{ attributes: make(map[string]interface{}), } // get unset attribute // Will return null without panicking // get non-int attribute // Will return 0 without panicking kws.SetAttribute("notInt", "") // get int attribute kws.SetAttribute("int", 3) v := kws.GetIntAttribute("int") require.Equal(t, 3, v) } func TestWebsocket_GetStringAttribute(t *testing.T) { kws := &Websocket{ attributes: make(map[string]interface{}), } // get unset attribute // get non-string attribute kws.SetAttribute("notString", 3) // get string attribute kws.SetAttribute("str", "3") v := kws.GetStringAttribute("str") require.Equal(t, "3", v) } func TestWebsocket_SetUUIDUpdatesPool(t *testing.T) { pool.reset() kws := createWS() pool.set(kws) oldUUID := kws.GetUUID() newUUID := "new-uuid" err := kws.SetUUID(newUUID) require.NoError(t, err) require.Equal(t, newUUID, kws.GetUUID()) _, err = pool.get(oldUUID) require.ErrorIs(t, err, ErrorInvalidConnection) poolEntry, err := pool.get(newUUID) require.NoError(t, err) require.Equal(t, kws, poolEntry) other := createWS() other.UUID = "other-uuid" pool.set(other) err = kws.SetUUID(other.UUID) require.ErrorIs(t, err, ErrorUUIDDuplication) require.Equal(t, newUUID, kws.GetUUID()) poolEntry, err = pool.get(newUUID) require.NoError(t, err) require.Equal(t, kws, poolEntry) } func assertPanic(t *testing.T, f func()) { defer func() { if r := recover(); r == nil { t.Errorf("The code did not panic") } }() f() } func createWS() *Websocket { kws := &Websocket{ Conn: nil, Locals: func(key string) interface{} { return "" }, Params: func(key string, defaultValue ...string) string { return "" }, Query: func(key string, defaultValue ...string) string { return "" }, Cookies: func(key string, defaultValue ...string) string { return "" }, queue: make(chan message), attributes: make(map[string]interface{}), isAlive: true, } kws.UUID = kws.createUUID() return kws } func upgradeMiddleware(c fiber.Ctx) error { // IsWebSocketUpgrade returns true if the client // requested upgrade to the WebSocket protocol. if fws.IsWebSocketUpgrade(c) { fiber.StoreInContext(c, "allowed", true) return c.Next() } return fiber.ErrUpgradeRequired } // // needed but not used // func (s *WebsocketMock) SetAttribute(_ string, _ interface{}) { panic("implement me") } func (s *WebsocketMock) GetAttribute(_ string) interface{} { panic("implement me") } func (s *WebsocketMock) EmitToList(_ []string, _ []byte, _ ...int) { panic("implement me") } func (s *WebsocketMock) EmitTo(_ string, _ []byte, _ ...int) error { panic("implement me") } func (s *WebsocketMock) Broadcast(_ []byte, _ bool, _ ...int) { panic("implement me") } func (s *WebsocketMock) Fire(_ string, _ []byte) { panic("implement me") } func (s *WebsocketMock) Close() { panic("implement me") } func (s *WebsocketMock) pong(_ context.Context) { panic("implement me") } func (s *WebsocketMock) write(_ int, _ []byte) { panic("implement me") } func (s *WebsocketMock) run() { panic("implement me") } func (s *WebsocketMock) read(_ context.Context) { panic("implement me") } func (s *WebsocketMock) disconnected(_ error) { panic("implement me") } func (s *WebsocketMock) createUUID() string { return s.randomUUID() } func (s *WebsocketMock) randomUUID() string { return uuid.New().String() } func (s *WebsocketMock) fireEvent(_ string, _ []byte, _ error) { panic("implement me") } ================================================ FILE: v3/swaggerui/README.md ================================================ --- id: swaggerui --- # Swagger UI > ⚠️ This module was renamed from `gofiber/contrib/swagger` to `swaggerui` to clearly distinguish it from the ported `swaggo` middleware. Update your imports accordingly. ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*swaggerui*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20swaggerui/badge.svg) Swagger UI middleware for [Fiber](https://github.com/gofiber/fiber). This handler serves pre-generated Swagger/OpenAPI specs via the swagger-ui package. **Compatible with Fiber v3.** ## 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. ### Table of Contents - [Signatures](#signatures) - [Installation](#installation) - [Examples](#examples) - [Config](#config) - [Default Config](#default-config) ### Signatures ```go func New(config ...swaggerui.Config) fiber.Handler ``` ### Installation Swagger 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: ```bash go mod init github.com// ``` And then install the Swagger UI middleware: ```bash go get github.com/gofiber/contrib/v3/swaggerui ``` ### Examples Import the middleware package ```go import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/swaggerui" ) ``` Using the default config: ```go app.Use(swaggerui.New()) ``` Using a custom config: ```go cfg := swaggerui.Config{ BasePath: "/", FilePath: "./docs/swagger.json", Path: "swagger", Title: "Swagger API Docs", } app.Use(swaggerui.New(cfg)) ``` Use program data for Swagger content: ```go cfg := swaggerui.Config{ BasePath: "/", FilePath: "./docs/swagger.json", FileContent: mySwaggerByteSlice, Path: "swagger", Title: "Swagger API Docs", } app.Use(swaggerui.New(cfg)) ``` Using multiple instances of Swagger: ```go // Create Swagger middleware for v1 // // Swagger will be available at: /api/v1/docs app.Use(swaggerui.New(swaggerui.Config{ BasePath: "/api/v1/", FilePath: "./docs/v1/swagger.json", Path: "docs", })) // Create Swagger middleware for a second API version // // Swagger will be available at: /api/v2/docs app.Use(swaggerui.New(swaggerui.Config{ BasePath: "/api/v2/", FilePath: "./docs/v2/swagger.json", Path: "docs", })) ``` ### Config ```go type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool // BasePath for the UI path // // Optional. Default: / BasePath string // FilePath for the swagger.json or swagger.yaml file // // Optional. Default: ./swagger.json FilePath string // FileContent for the content of the swagger.json or swagger.yaml file. // If provided, FilePath will not be read. // // Optional. Default: nil FileContent []byte // Path combines with BasePath for the full UI path // // Optional. Default: docs Path string // Title for the documentation site // // Optional. Default: Fiber API documentation Title string // CacheAge defines the max-age for the Cache-Control header in seconds. // // Optional. Default: 3600 (1 hour) CacheAge int } ``` ### Default Config ```go var ConfigDefault = Config{ Next: nil, BasePath: "/", FilePath: "./swagger.json", Path: "docs", Title: "Fiber API documentation", CacheAge: 3600, // Default to 1 hour } ``` ================================================ FILE: v3/swaggerui/go.mod ================================================ module github.com/gofiber/contrib/v3/swaggerui go 1.25.0 require ( github.com/go-openapi/runtime v0.29.4 github.com/gofiber/fiber/v3 v3.1.0 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-openapi/analysis v0.25.0 // indirect github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/loads v0.23.3 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/strfmt v0.26.1 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect github.com/go-openapi/swag/fileutils v0.26.0 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect github.com/go-openapi/swag/loading v0.26.0 // indirect github.com/go-openapi/swag/mangling v0.26.0 // indirect github.com/go-openapi/swag/stringutils v0.26.0 // indirect github.com/go-openapi/swag/typeutils v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-openapi/validate v0.25.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/swaggerui/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= github.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo= github.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/swaggerui/swagger.go ================================================ package swaggerui import ( "encoding/json" "fmt" "log" "net/http" "os" "path" "strings" "github.com/go-openapi/runtime/middleware" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/adaptor" "gopkg.in/yaml.v2" ) // Config defines the config for middleware. type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool // BasePath for the UI path // // Optional. Default: / BasePath string // FilePath for the swagger.json or swagger.yaml file // // Optional. Default: ./swagger.json FilePath string // FileContent for the content of the swagger.json or swagger.yaml file. // If provided, FilePath will not be read. // // Optional. Default: nil FileContent []byte // Path combines with BasePath for the full UI path // // Optional. Default: docs Path string // Title for the documentation site // // Optional. Default: Fiber API documentation Title string // CacheAge defines the max-age for the Cache-Control header in seconds. // // Optional. Default: 3600 (1 hour) CacheAge int // The three components needed to embed swagger-ui // SwaggerURL points to the js that generates the SwaggerUI site. // // Defaults to: https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js SwaggerURL string SwaggerPresetURL string SwaggerStylesURL string Favicon32 string Favicon16 string } // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, BasePath: "/", FilePath: "./swagger.json", Path: "docs", Title: "Fiber API documentation", CacheAge: 3600, // Default to 1 hour } // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := ConfigDefault // Override config if provided if len(config) > 0 { cfg = config[0] // Set default values if len(cfg.BasePath) == 0 { cfg.BasePath = ConfigDefault.BasePath } if len(cfg.FilePath) == 0 { cfg.FilePath = ConfigDefault.FilePath } if len(cfg.Path) == 0 { cfg.Path = ConfigDefault.Path } if len(cfg.Title) == 0 { cfg.Title = ConfigDefault.Title } if cfg.CacheAge == 0 { cfg.CacheAge = ConfigDefault.CacheAge } } rawSpec := cfg.FileContent if len(rawSpec) == 0 { // Verify Swagger file exists _, err := os.Stat(cfg.FilePath) if os.IsNotExist(err) { panic(fmt.Errorf("%s file does not exist", cfg.FilePath)) } // Read Swagger Spec into memory rawSpec, err = os.ReadFile(cfg.FilePath) if err != nil { log.Fatalf("Failed to read provided Swagger file (%s): %v", cfg.FilePath, err.Error()) panic(err) } } // Validate we have valid JSON or YAML var jsonData map[string]interface{} errJSON := json.Unmarshal(rawSpec, &jsonData) var yamlData map[string]interface{} errYAML := yaml.Unmarshal(rawSpec, &yamlData) if errJSON != nil && errYAML != nil { log.Fatalf("Failed to parse the Swagger spec as JSON or YAML: JSON error: %s, YAML error: %s", errJSON, errYAML) if len(cfg.FileContent) != 0 { panic(fmt.Errorf("Invalid Swagger spec: %s", string(rawSpec))) } panic(fmt.Errorf("Invalid Swagger spec file: %s", cfg.FilePath)) } // Generate URL path's for the middleware specURL := path.Join(cfg.BasePath, cfg.FilePath) swaggerUIPath := path.Join(cfg.BasePath, cfg.Path) // Serve the Swagger spec from memory swaggerSpecHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, ".yaml") || strings.HasSuffix(r.URL.Path, ".yml") { w.Header().Set("Content-Type", "application/yaml") w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", cfg.CacheAge)) _, err := w.Write(rawSpec) if err != nil { http.Error(w, "Error processing YAML Swagger Spec", http.StatusInternalServerError) return } } else if strings.HasSuffix(r.URL.Path, ".json") { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", cfg.CacheAge)) _, err := w.Write(rawSpec) if err != nil { http.Error(w, "Error processing JSON Swagger Spec", http.StatusInternalServerError) return } } else { http.NotFound(w, r) } }) // Define UI Options swaggerUIOpts := middleware.SwaggerUIOpts{ BasePath: cfg.BasePath, SpecURL: specURL, Path: cfg.Path, Title: cfg.Title, } if cfg.SwaggerURL != "" { swaggerUIOpts.SwaggerURL = cfg.SwaggerURL } if cfg.SwaggerPresetURL != "" { swaggerUIOpts.SwaggerPresetURL = cfg.SwaggerPresetURL } if cfg.SwaggerStylesURL != "" { swaggerUIOpts.SwaggerStylesURL = cfg.SwaggerStylesURL } if cfg.Favicon32 != "" { swaggerUIOpts.Favicon32 = cfg.Favicon32 } if cfg.Favicon16 != "" { swaggerUIOpts.Favicon16 = cfg.Favicon16 } // Create UI middleware middlewareHandler := adaptor.HTTPHandler(middleware.SwaggerUI(swaggerUIOpts, swaggerSpecHandler)) // Return new handler return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } // Only respond to requests to SwaggerUI and SpecURL (swagger.json) if c.Path() != swaggerUIPath && c.Path() != specURL { return c.Next() } // Pass Fiber context to handler return middlewareHandler(c) } } ================================================ FILE: v3/swaggerui/swagger.json ================================================ { "consumes": [ "application/json" ], "produces": [ "application/json" ], "schemes": [ "http" ], "swagger": "2.0", "info": { "description": "Documentation for TestApi", "title": "TestApi", "version": "1.0.0" }, "basePath": "/" } ================================================ FILE: v3/swaggerui/swagger.yaml ================================================ consumes: - application/json produces: - application/json schemes: - http swagger: "2.0" info: description: "Documentation for TestApi" title: "TestApi" version: "1.0.0" basePath: "/" ================================================ FILE: v3/swaggerui/swagger_missing.json ================================================ { "consumes": [ "application/json" ], "produces": [ "application/json" ], "schemes": [ "http" ], "swagger": "info": { "description": "Documentation for TestApi", "title": "TestApi", "version": "1.0.0" }, "basePath": "/" } ================================================ FILE: v3/swaggerui/swagger_test.go ================================================ package swaggerui import ( _ "embed" "io" "net/http" "net/http/httptest" "testing" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" ) //go:embed swagger.json var swaggerJSON []byte //go:embed swagger.yaml var swaggerYAML []byte func performRequest(method, target string, app *fiber.App) *http.Response { r := httptest.NewRequest(method, target, nil) resp, _ := app.Test(r, fiber.TestConfig{Timeout: 0, FailOnTimeout: false}) return resp } func TestNew(t *testing.T) { t.Run("Endpoint check with only custom path", func(t *testing.T) { app := fiber.New() cfg := Config{ Path: "custompath", } app.Use(New(cfg)) w1 := performRequest("GET", "/custompath", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with only custom basepath", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/api/v1", } app.Use(New(cfg)) w1 := performRequest("GET", "/api/v1/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/api/v1/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom config", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "swagger.json", } app.Use(New(cfg)) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom path", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "swagger.json", Path: "swagger", } app.Use(New(cfg)) w1 := performRequest("GET", "/swagger", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom config and yaml spec", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "./swagger.yaml", } app.Use(New(cfg)) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.yaml", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom path and yaml spec", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "swagger.yaml", Path: "swagger", } app.Use(New(cfg)) w1 := performRequest("GET", "/swagger", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.yaml", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with empty custom config", func(t *testing.T) { app := fiber.New() cfg := Config{} app.Use(New(cfg)) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with default config", func(t *testing.T) { app := fiber.New() app.Use(New()) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Swagger.json file is not exist", func(t *testing.T) { app := fiber.New() cfg := Config{ FilePath: "./docs/swagger.json", } require.Panics(t, func() { app.Use(New(cfg)) }, "/swagger.json file is not exist") }) t.Run("Swagger.json missing file", func(t *testing.T) { app := fiber.New() cfg := Config{ FilePath: "./docs/swagger_missing.json", } require.Panics(t, func() { app.Use(New(cfg)) }, "invalid character ':' after object key:value pair") }) t.Run("Endpoint check with multiple Swagger instances", func(t *testing.T) { app := fiber.New() app.Use(New(Config{ BasePath: "/api/v1", })) app.Use(New(Config{ BasePath: "/api/v2", })) w1 := performRequest("GET", "/api/v1/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/api/v1/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/api/v2/docs", app) require.Equal(t, 200, w3.StatusCode) w4 := performRequest("GET", "/api/v2/swagger.json", app) require.Equal(t, 200, w4.StatusCode) w5 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w5.StatusCode) }) t.Run("Endpoint check with custom routes", func(t *testing.T) { app := fiber.New() app.Use(New(Config{ BasePath: "/api/v1", })) app.Get("/api/v1/tasks", func(c fiber.Ctx) error { return c.SendString("success") }) app.Get("/api/v1", func(c fiber.Ctx) error { return c.SendString("success") }) w1 := performRequest("GET", "/api/v1/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/api/v1/swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) // Verify we can send request to handler with the same BasePath as the middleware w4 := performRequest("GET", "/api/v1/tasks", app) bodyBytes, err := io.ReadAll(w4.Body) require.NoError(t, err) require.Equal(t, 200, w4.StatusCode) require.Equal(t, "success", string(bodyBytes)) // Verify handler in BasePath still works w5 := performRequest("GET", "/api/v1", app) bodyBytes, err = io.ReadAll(w5.Body) require.NoError(t, err) require.Equal(t, 200, w5.StatusCode) require.Equal(t, "success", string(bodyBytes)) w6 := performRequest("GET", "/api/v1/", app) bodyBytes, err = io.ReadAll(w6.Body) require.NoError(t, err) require.Equal(t, 200, w6.StatusCode) require.Equal(t, "success", string(bodyBytes)) }) } func TestNewWithFileContent(t *testing.T) { t.Run("Endpoint check with only custom path", func(t *testing.T) { app := fiber.New() cfg := Config{ Path: "custompath", FileContent: swaggerJSON, FilePath: "doesnotexist-swagger.json", } app.Use(New(cfg)) w1 := performRequest("GET", "/custompath", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with only custom basepath", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/api/v1", FileContent: swaggerJSON, FilePath: "doesnotexist-swagger.json", } app.Use(New(cfg)) w1 := performRequest("GET", "/api/v1/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/api/v1/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom config", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "doesnotexist-swagger.json", FileContent: swaggerJSON, } app.Use(New(cfg)) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom path", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "doesnotexist-swagger.json", Path: "swagger", FileContent: swaggerJSON, } app.Use(New(cfg)) w1 := performRequest("GET", "/swagger", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom config and yaml spec", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "./doesnotexist-swagger.yaml", FileContent: swaggerYAML, } app.Use(New(cfg)) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/doesnotexist-swagger.yaml", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with custom path and yaml spec", func(t *testing.T) { app := fiber.New() cfg := Config{ BasePath: "/", FilePath: "doesnotexist-swagger.yaml", Path: "swagger", FileContent: swaggerYAML, } app.Use(New(cfg)) w1 := performRequest("GET", "/swagger", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/doesnotexist-swagger.yaml", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Endpoint check with empty custom config", func(t *testing.T) { app := fiber.New() cfg := Config{ FileContent: swaggerJSON, FilePath: "doesnotexist-swagger.json", } app.Use(New(cfg)) w1 := performRequest("GET", "/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) }) t.Run("Swagger file content not specified", func(t *testing.T) { app := fiber.New() cfg := Config{ FilePath: "./docs/swagger.json", } require.Panics(t, func() { app.Use(New(cfg)) }, "content not specified") }) t.Run("Endpoint check with multiple Swagger instances", func(t *testing.T) { app := fiber.New() app.Use(New(Config{ BasePath: "/api/v1", FileContent: swaggerJSON, FilePath: "doesnotexist-swagger.json", })) app.Use(New(Config{ BasePath: "/api/v2", FileContent: swaggerJSON, FilePath: "doesnotexist-swagger.json", })) w1 := performRequest("GET", "/api/v1/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/api/v1/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/api/v2/docs", app) require.Equal(t, 200, w3.StatusCode) w4 := performRequest("GET", "/api/v2/doesnotexist-swagger.json", app) require.Equal(t, 200, w4.StatusCode) w5 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w5.StatusCode) }) t.Run("Endpoint check with custom routes", func(t *testing.T) { app := fiber.New() app.Use(New(Config{ BasePath: "/api/v1", FileContent: swaggerJSON, FilePath: "doesnotexist-swagger.json", })) app.Get("/api/v1/tasks", func(c fiber.Ctx) error { return c.SendString("success") }) app.Get("/api/v1", func(c fiber.Ctx) error { return c.SendString("success") }) w1 := performRequest("GET", "/api/v1/docs", app) require.Equal(t, 200, w1.StatusCode) w2 := performRequest("GET", "/api/v1/doesnotexist-swagger.json", app) require.Equal(t, 200, w2.StatusCode) w3 := performRequest("GET", "/notfound", app) require.Equal(t, 404, w3.StatusCode) // Verify we can send request to handler with the same BasePath as the middleware w4 := performRequest("GET", "/api/v1/tasks", app) bodyBytes, err := io.ReadAll(w4.Body) require.NoError(t, err) require.Equal(t, 200, w4.StatusCode) require.Equal(t, "success", string(bodyBytes)) // Verify handler in BasePath still works w5 := performRequest("GET", "/api/v1", app) bodyBytes, err = io.ReadAll(w5.Body) require.NoError(t, err) require.Equal(t, 200, w5.StatusCode) require.Equal(t, "success", string(bodyBytes)) w6 := performRequest("GET", "/api/v1/", app) bodyBytes, err = io.ReadAll(w6.Body) require.NoError(t, err) require.Equal(t, 200, w6.StatusCode) require.Equal(t, "success", string(bodyBytes)) }) } ================================================ FILE: v3/swaggo/README.md ================================================ --- id: swaggo --- # Swaggo > ⚠️ This module was renamed from `gofiber/swagger` to `swaggo` to clearly distinguish it from the ported `swaggerui` middleware. Update your imports accordingly. ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*swaggo*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/actions/workflows/test-swaggo.yml/badge.svg) Swaggo 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. **Compatible with Fiber v3.** ## 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. ### Table of Contents - [Signatures](#signatures) - [Installation](#installation) - [Usage](#usage) - [Config](#config) - [Default Config](#default-config) ### Signatures ```go var HandlerDefault = New() func New(config ...Config) fiber.Handler ``` ### Installation Swagger 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: ```bash go mod init github.com// ``` Then install the middleware: ```bash go get github.com/gofiber/contrib/v3/swaggo ``` ### Usage First, document your API using swaggo/swag comments and generate the documentation files (usually inside a `docs` package) by running `swag init`. ```go package main import ( "github.com/gofiber/fiber/v3" swaggo "github.com/gofiber/contrib/v3/swaggo" // docs are generated by Swag CLI, you have to import them. // Replace with your own docs folder, usually "github.com/username/reponame/docs". _ "github.com/username/reponame/docs" ) func main() { app := fiber.New() // Mount the UI with the default configuration under /swagger app.Get("/swagger/*", swaggo.HandlerDefault) // Customize the UI by passing a Config app.Get("/docs/*", swaggo.New(swaggo.Config{ URL: "http://example.com/doc.json", DeepLinking: false, DocExpansion: "none", OAuth2RedirectUrl: "http://localhost:8080/swagger/oauth2-redirect.html", })) app.Listen(":8080") } ``` ### Config ```go type Config struct { InstanceName string Title string ConfigURL string URL string QueryConfigEnabled bool Layout string Plugins []template.JS Presets []template.JS DeepLinking bool DisplayOperationId bool DefaultModelsExpandDepth int DefaultModelExpandDepth int DefaultModelRendering string DisplayRequestDuration bool DocExpansion string Filter FilterConfig MaxDisplayedTags int ShowExtensions bool ShowCommonExtensions bool TagsSorter template.JS OnComplete template.JS SyntaxHighlight *SyntaxHighlightConfig TryItOutEnabled bool RequestSnippetsEnabled bool OAuth2RedirectUrl string RequestInterceptor template.JS RequestCurlOptions []string ResponseInterceptor template.JS ShowMutatedRequest bool SupportedSubmitMethods []string ValidatorUrl string WithCredentials bool ModelPropertyMacro template.JS ParameterMacro template.JS PersistAuthorization bool OAuth *OAuthConfig PreauthorizeBasic template.JS PreauthorizeApiKey template.JS CustomStyle template.CSS CustomScript template.JS } ``` ### Default Config ```go var ConfigDefault = Config{ Title: "Swagger UI", Layout: "StandaloneLayout", URL: "doc.json", DeepLinking: true, ShowMutatedRequest: true, Plugins: []template.JS{ template.JS("SwaggerUIBundle.plugins.DownloadUrl"), }, Presets: []template.JS{ template.JS("SwaggerUIBundle.presets.apis"), template.JS("SwaggerUIStandalonePreset"), }, SyntaxHighlight: &SyntaxHighlightConfig{Activate: true, Theme: "agate"}, } ``` > Refer to `config.go` for a complete list of options and documentation strings. ================================================ FILE: v3/swaggo/config.go ================================================ package swaggo import ( "html/template" ) // Config stores SwaggerUI configuration variables type Config struct { // This parameter can be used to name different swagger document instances. // default: "" InstanceName string `json:"-"` // Title pointing to title of HTML page. // default: "Swagger UI" Title string `json:"-"` // URL to fetch external configuration document from. // default: "" ConfigURL string `json:"configUrl,omitempty"` // The URL pointing to API definition (normally swagger.json or swagger.yaml). // default: "doc.json" URL string `json:"url,omitempty"` // Enables overriding configuration parameters via URL search params. // default: false QueryConfigEnabled bool `json:"queryConfigEnabled,omitempty"` // The name of a component available via the plugin system to use as the top-level layout for Swagger UI. // default: "StandaloneLayout" Layout string `json:"layout,omitempty"` // An array of plugin functions to use in Swagger UI. // default: [SwaggerUIBundle.plugins.DownloadUrl] Plugins []template.JS `json:"-"` // An array of presets to use in Swagger UI. Usually, you'll want to include ApisPreset if you use this option. // default: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset] Presets []template.JS `json:"-"` // If set to true, enables deep linking for tags and operations. // default: true DeepLinking bool `json:"deepLinking"` // Controls the display of operationId in operations list. // default: false DisplayOperationId bool `json:"displayOperationId,omitempty"` // The default expansion depth for models (set to -1 completely hide the models). // default: 1 DefaultModelsExpandDepth int `json:"defaultModelsExpandDepth,omitempty"` // The default expansion depth for the model on the model-example section. // default: 1 DefaultModelExpandDepth int `json:"defaultModelExpandDepth,omitempty"` // Controls how the model is shown when the API is first rendered. // The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links. // default: "example" DefaultModelRendering string `json:"defaultModelRendering,omitempty"` // Controls the display of the request duration (in milliseconds) for "Try it out" requests. // default: false DisplayRequestDuration bool `json:"displayRequestDuration,omitempty"` // Controls the default expansion setting for the operations and tags. // 'list' (default, expands only the tags), // 'full' (expands the tags and operations), // 'none' (expands nothing) DocExpansion string `json:"docExpansion,omitempty"` // If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. // Can be Boolean to enable or disable, or a string, in which case filtering will be enabled using that string as the filter expression. // Filtering is case sensitive matching the filter expression anywhere inside the tag. // default: false Filter FilterConfig `json:"-"` // If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations. // default: 0 MaxDisplayedTags int `json:"maxDisplayedTags,omitempty"` // Controls the display of vendor extension (x-) fields and values for Operations, Parameters, Responses, and Schema. // default: false ShowExtensions bool `json:"showExtensions,omitempty"` // Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters. // default: false ShowCommonExtensions bool `json:"showCommonExtensions,omitempty"` // 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(). // to learn how to write a sort function). Two tag name strings are passed to the sorter for each pass. // default: "" -> Default is the order determined by Swagger UI. TagsSorter template.JS `json:"-"` // Provides a mechanism to be notified when Swagger UI has finished rendering a newly provided definition. // default: "" -> Function=NOOP OnComplete template.JS `json:"-"` // An object with the activate and theme properties. SyntaxHighlight *SyntaxHighlightConfig `json:"-"` // Controls whether the "Try it out" section should be enabled by default. // default: false TryItOutEnabled bool `json:"tryItOutEnabled,omitempty"` // Enables the request snippet section. When disabled, the legacy curl snippet will be used. // default: false RequestSnippetsEnabled bool `json:"requestSnippetsEnabled,omitempty"` // OAuth redirect URL. // default: "" OAuth2RedirectUrl string `json:"oauth2RedirectUrl,omitempty"` // MUST be a function. Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. // Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. // default: "" RequestInterceptor template.JS `json:"-"` // 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. // For example request.curlOptions = ["-g", "--limit-rate 20k"] // default: nil RequestCurlOptions []string `json:"request.curlOptions,omitempty"` // MUST be a function. Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. // Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. // default: "" ResponseInterceptor template.JS `json:"-"` // If set to true, uses the mutated request returned from a requestInterceptor to produce the curl command in the UI, // otherwise the request before the requestInterceptor was applied is used. // default: true ShowMutatedRequest bool `json:"showMutatedRequest"` // List of HTTP methods that have the "Try it out" feature enabled. An empty array disables "Try it out" for all operations. // This does not filter the operations from the display. // Possible values are ["get", "put", "post", "delete", "options", "head", "patch", "trace"] // default: nil SupportedSubmitMethods []string `json:"supportedSubmitMethods,omitempty"` // 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. // For example for locally deployed validators (https://github.com/swagger-api/validator-badge). // Setting it to either none, 127.0.0.1 or localhost will disable validation. // default: "" ValidatorUrl string `json:"validatorUrl,omitempty"` // If set to true, enables passing credentials, as defined in the Fetch standard, in CORS requests that are sent by the browser. // Note that Swagger UI cannot currently set cookies cross-domain (see https://github.com/swagger-api/swagger-js/issues/1163). // as a result, you will have to rely on browser-supplied cookies (which this setting enables sending) that Swagger UI cannot control. // default: false WithCredentials bool `json:"withCredentials,omitempty"` // Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable. // default: "" ModelPropertyMacro template.JS `json:"-"` // Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter). // Operation and parameter are objects passed for context, both remain immutable. // default: "" ParameterMacro template.JS `json:"-"` // If set to true, it persists authorization data and it would not be lost on browser close/refresh. // default: false PersistAuthorization bool `json:"persistAuthorization,omitempty"` // Configuration information for OAuth2, optional if using OAuth2 OAuth *OAuthConfig `json:"-"` // (authDefinitionKey, username, password) => action // Programmatically set values for a Basic authorization scheme. // default: "" PreauthorizeBasic template.JS `json:"-"` // (authDefinitionKey, apiKeyValue) => action // Programmatically set values for an API key or Bearer authorization scheme. // In case of OpenAPI 3.0 Bearer scheme, apiKeyValue must contain just the token itself without the Bearer prefix. // default: "" PreauthorizeApiKey template.JS `json:"-"` // Applies custom CSS styles. // default: "" CustomStyle template.CSS `json:"-"` // Applies custom JavaScript scripts. // default "" CustomScript template.JS `json:"-"` } type FilterConfig struct { Enabled bool Expression string } func (fc FilterConfig) Value() interface{} { if fc.Expression != "" { return fc.Expression } return fc.Enabled } type SyntaxHighlightConfig struct { // Whether syntax highlighting should be activated or not. // default: true Activate bool `json:"activate"` // Highlight.js syntax coloring theme to use. // Possible values are ["agate", "arta", "monokai", "nord", "obsidian", "tomorrow-night"] // default: "agate" Theme string `json:"theme,omitempty"` } func (shc SyntaxHighlightConfig) Value() interface{} { if shc.Activate { return shc } return false } type OAuthConfig struct { // ID of the client sent to the OAuth2 provider. // default: "" ClientId string `json:"clientId,omitempty"` // Never use this parameter in your production environment. // It exposes crucial security information. This feature is intended for dev/test environments only. // Secret of the client sent to the OAuth2 provider. // default: "" ClientSecret string `json:"clientSecret,omitempty"` // Application name, displayed in authorization popup. // default: "" AppName string `json:"appName,omitempty"` // Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl. // default: "" Realm string `json:"realm,omitempty"` // String array of initially selected oauth scopes // default: nil Scopes []string `json:"scopes,omitempty"` // Additional query parameters added to authorizationUrl and tokenUrl. // default: nil AdditionalQueryStringParams map[string]string `json:"additionalQueryStringParams,omitempty"` // Unavailable Only activated for the accessCode flow. // During the authorization_code request to the tokenUrl, pass the Client Password using the HTTP Basic Authentication scheme // (Authorization header with Basic base64encode(client_id + client_secret)). // default: false UseBasicAuthenticationWithAccessCodeGrant bool `json:"useBasicAuthenticationWithAccessCodeGrant,omitempty"` // Only applies to authorizationCode flows. // Proof Key for Code Exchange brings enhanced security for OAuth public clients. // default: false UsePkceWithAuthorizationCodeGrant bool `json:"usePkceWithAuthorizationCodeGrant,omitempty"` } var ( ConfigDefault = Config{ Title: "Swagger UI", Layout: "StandaloneLayout", Plugins: []template.JS{ template.JS("SwaggerUIBundle.plugins.DownloadUrl"), }, Presets: []template.JS{ template.JS("SwaggerUIBundle.presets.apis"), template.JS("SwaggerUIStandalonePreset"), }, DeepLinking: true, DefaultModelsExpandDepth: 1, DefaultModelExpandDepth: 1, DefaultModelRendering: "example", DocExpansion: "list", SyntaxHighlight: &SyntaxHighlightConfig{ Activate: true, Theme: "agate", }, ShowMutatedRequest: true, } ) // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided if len(config) < 1 { return ConfigDefault } // Override default config cfg := config[0] if cfg.Title == "" { cfg.Title = ConfigDefault.Title } if cfg.Layout == "" { cfg.Layout = ConfigDefault.Layout } if cfg.DefaultModelRendering == "" { cfg.DefaultModelRendering = ConfigDefault.DefaultModelRendering } if cfg.DocExpansion == "" { cfg.DocExpansion = ConfigDefault.DocExpansion } if cfg.Plugins == nil { cfg.Plugins = ConfigDefault.Plugins } if cfg.Presets == nil { cfg.Presets = ConfigDefault.Presets } if cfg.SyntaxHighlight == nil { cfg.SyntaxHighlight = ConfigDefault.SyntaxHighlight } return cfg } ================================================ FILE: v3/swaggo/go.mod ================================================ module github.com/gofiber/contrib/v3/swaggo go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/swaggo/files/v2 v2.0.2 github.com/swaggo/swag v1.16.6 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/swag/conv v0.26.0 // indirect github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-openapi/swag/jsonutils v0.26.0 // indirect github.com/go-openapi/swag/loading v0.26.0 // indirect github.com/go-openapi/swag/stringutils v0.26.0 // indirect github.com/go-openapi/swag/typeutils v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/tools v0.44.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) ================================================ FILE: v3/swaggo/go.sum ================================================ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/swaggo/index.go ================================================ package swaggo const indexTmpl string = ` {{.Title}} {{- if .CustomStyle}} {{- end}} {{- if .CustomScript}} {{- end}}
` ================================================ FILE: v3/swaggo/swagger.go ================================================ package swaggo import ( "errors" "fmt" "html/template" "io" "io/fs" "path" "strings" "sync" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" swaggerFiles "github.com/swaggo/files/v2" "github.com/swaggo/swag" ) const ( defaultDocURL = "doc.json" defaultIndex = "index.html" ) var HandlerDefault = New() // New returns custom handler func New(config ...Config) fiber.Handler { cfg := configDefault(config...) index, err := template.New("swagger_index.html").Parse(indexTmpl) if err != nil { panic(fmt.Errorf("fiber: swagger middleware error -> %w", err)) } var ( basePrefix string once sync.Once ) return func(c fiber.Ctx) error { once.Do(func() { basePrefix = strings.ReplaceAll(c.Route().Path, "*", "") }) prefix := basePrefix if forwardedPrefix := getForwardedPrefix(c); forwardedPrefix != "" { prefix = forwardedPrefix + prefix } cfgCopy := cfg if len(cfgCopy.URL) == 0 { cfgCopy.URL = path.Join(prefix, defaultDocURL) } p := utils.CopyString(c.Params("*")) switch p { case defaultIndex: c.Type("html") return index.Execute(c, cfgCopy) case defaultDocURL: var doc string if doc, err = swag.ReadDoc(cfgCopy.InstanceName); err != nil { return err } return c.Type("json").SendString(doc) case "", "/": return c.Redirect().Status(fiber.StatusMovedPermanently).To(path.Join(prefix, defaultIndex)) default: filePath := path.Clean("/" + p) filePath = strings.TrimPrefix(filePath, "/") if filePath == "" { return fiber.ErrNotFound } file, err := swaggerFiles.FS.Open(filePath) if err != nil { if errors.Is(err, fs.ErrNotExist) { return fiber.ErrNotFound } return err } defer func() { cerr := file.Close() if cerr != nil && err == nil { err = cerr } }() info, err := file.Stat() if err != nil { if errors.Is(err, fs.ErrNotExist) { return fiber.ErrNotFound } return err } if info.IsDir() { return fiber.ErrNotFound } data, err := io.ReadAll(file) if err != nil { return err } if ext := strings.TrimPrefix(path.Ext(filePath), "."); ext != "" { c.Type(ext) } return c.Send(data) } } } func getForwardedPrefix(c fiber.Ctx) string { header := c.GetReqHeaders()["X-Forwarded-Prefix"] if len(header) == 0 { return "" } prefix := "" for _, rawPrefix := range header { endIndex := len(rawPrefix) for endIndex > 1 && rawPrefix[endIndex-1] == '/' { endIndex-- } prefix += rawPrefix[:endIndex] } return prefix } ================================================ FILE: v3/swaggo/swagger_test.go ================================================ package swaggo import ( "mime" "net/http" "sync" "testing" "github.com/gofiber/fiber/v3" "github.com/swaggo/swag" ) type mockedSwag struct{} func (s *mockedSwag) ReadDoc() string { return `{ "swagger": "2.0", "info": { "description": "This is a sample server.", "title": "Swagger Example API", "termsOfService": "http://swagger.io/terms/", "contact": { "name": "API Support", "url": "http://www.swagger.io/support", "email": "support@swagger.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "1.0" }, "host": "petstore.swagger.io", "basePath": "/v2", "paths": {} }` } var ( registrationOnce sync.Once ) func Test_Swagger(t *testing.T) { app := fiber.New() registrationOnce.Do(func() { swag.Register(swag.Name, &mockedSwag{}) }) app.Get("/swag/*", HandlerDefault) tests := []struct { name string url string statusCode int contentType string location string }{ { name: "Should be returns status 200 with 'text/html' content-type", url: "/swag/index.html", statusCode: 200, contentType: "text/html", }, { name: "Should be returns status 200 with 'application/json' content-type", url: "/swag/doc.json", statusCode: 200, contentType: "application/json", }, { name: "Should be returns status 200 with 'image/png' content-type", url: "/swag/favicon-16x16.png", statusCode: 200, contentType: "image/png", }, { name: "Should return status 301", url: "/swag/", statusCode: 301, location: "/swag/index.html", }, { name: "Should return status 404", url: "/swag/notfound", statusCode: 404, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, tt.url, nil) if err != nil { t.Fatal(err) } req.Host = "localhost" resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != tt.statusCode { t.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, tt.statusCode) } if tt.contentType != "" { ct := resp.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(ct) if err != nil { t.Fatalf("invalid content type %q: %v", ct, err) } if mediaType != tt.contentType { t.Fatalf(`Content-Type: got %s - expected %s`, mediaType, tt.contentType) } } if tt.location != "" { location := resp.Header.Get("Location") if location != tt.location { t.Fatalf(`Location: got %s - expected %s`, location, tt.location) } } }) } } func Test_Swagger_Proxy_Redirect(t *testing.T) { app := fiber.New() registrationOnce.Do(func() { swag.Register(swag.Name, &mockedSwag{}) }) // Use new handler since the prefix is created only once per handler app.Get("/swag/*", New()) statusCode := 301 location := "/custom/path/swag/index.html" t.Run("Should return status 301 with proxy redirect", func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "/swag/", nil) if err != nil { t.Fatal(err) } req.Host = "localhost" req.Header.Set("X-Forwarded-Prefix", "/custom/path/") resp, err := app.Test(req) if err != nil { t.Fatal(err) } if resp.StatusCode != statusCode { t.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, statusCode) } if location != "" { responseLocation := resp.Header.Get("Location") if responseLocation != location { t.Fatalf(`Location: got %s - expected %s`, responseLocation, location) } } }) } ================================================ FILE: v3/testcontainers/README.md ================================================ --- id: testcontainers --- # Testcontainers ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*testcontainers*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20Testcontainers%20Services/badge.svg) A [Testcontainers](https://golang.testcontainers.org/) Service Implementation for Fiber. :::note Requires Go **1.25** and above ::: **Compatible with Fiber v3.** ## 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. > Test requirement: integration tests for this package require a reachable Docker daemon. ## Common Use Cases - Local development - Integration testing - Isolated service testing - End-to-end testing ## Install :::caution This Service Implementation only supports Fiber **v3**. ::: ```shell go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/testcontainers ``` ## Signature ### NewModuleConfig ```go // NewModuleConfig creates a new container service config for a module. // // - The serviceKey is the key used to identify the service in the Fiber app's state. // - The img is the image name to use for the container. // - 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]. // - The opts are the functional options to pass to the run function. This argument is optional. func NewModuleConfig[T testcontainers.Container]( serviceKey string, img string, run func(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (T, error), opts ...testcontainers.ContainerCustomizer, ) Config[T] { ``` ### NewContainerConfig ```go // NewContainerConfig creates a new container service config for a generic container type, // not created by a Testcontainers module. So this function best used in combination with // the [AddService] function to add a custom container to the Fiber app's state. // // - The serviceKey is the key used to identify the service in the Fiber app's state. // - The img is the image name to use for the container. // - The opts are the functional options to pass to the [testcontainers.Run] function. This argument is optional. // // This function uses the [testcontainers.Run] function as the run function. func NewContainerConfig[T *testcontainers.DockerContainer](serviceKey string, img string, opts ...testcontainers.ContainerCustomizer) Config[*testcontainers.DockerContainer] ``` ### AddService ```go // AddService adds a Testcontainers container as a [fiber.Service] for the Fiber app. // It returns a pointer to a [ContainerService[T]] object, which contains the key used to identify // the service in the Fiber app's state, and an error if the config is nil. // The container should be a function like redis.Run or postgres.Run that returns a container type // which embeds [testcontainers.Container]. // - The cfg is the Fiber app's configuration, needed to add the service to the Fiber app's state. // - The containerConfig is the configuration for the container, where: // - The containerConfig.ServiceKey is the key used to identify the service in the Fiber app's state. // - 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. // - The containerConfig.Image is the image to use for the container. // - The containerConfig.Options are the functional options to pass to the [testcontainers.Run] function. This argument is optional. // // Use [NewModuleConfig] or [NewContainerConfig] helper functions to create valid containerConfig objects. func AddService[T testcontainers.Container](cfg *fiber.Config, containerConfig Config[T]) (*ContainerService[T], error) { ``` ## Types ### Config The `Config` type is a generic type that is used to configure the container. | Property | Type | Description | Default | |-------------|------|-------------|---------| | ServiceKey | string | The key used to identify the service in the Fiber app's state. | - | | Image | string | The image name to use for the container. | - | | 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 | - | | Options | []testcontainers.ContainerCustomizer | The functional options to pass to the [testcontainers.Run] function. This argument is optional. | - | ```go // Config contains the configuration for a container service. type Config[T testcontainers.Container] struct { // ServiceKey is the key used to identify the service in the Fiber app's state. ServiceKey string // Image is the image name to use for the container. Image string // Run is 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, // although it could be the generic [testcontainers.Run] function from the testcontainers-go package. Run func(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (T, error) // Options are the functional options to pass to the [testcontainers.Run] function. This argument is optional. // You can find the available options in the [testcontainers website]. // // [testcontainers website]: https://golang.testcontainers.org/features/creating_container/#customizing-the-container Options []testcontainers.ContainerCustomizer } ``` ### ContainerService The `ContainerService` type is a generic type that embeds a [testcontainers.Container](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#Container) interface, and implements the [fiber.Service] interface, thanks to the Start, String, State and Terminate methods. It manages the lifecycle of a `testcontainers.Container` instance, and it can be retrieved from the Fiber app's state calling the `fiber.MustGetService` function with the key returned by the `ContainerService.Key` method. The type parameter `T` must implement the [testcontainers.Container](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#Container) interface, as in the Testcontainers Go modules (e.g. [redis.RedisContainer](https://pkg.go.dev/github.com/testcontainers/testcontainers-go/modules/redis#RedisContainer), [postgres.PostgresContainer](https://pkg.go.dev/github.com/testcontainers/testcontainers-go/modules/postgres#PostgresContainer), etc.), or in the generic [testcontainers.DockerContainer](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#GenericContainer) type, used for custom containers. :::note Since `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. ::: ```go type ContainerService[T testcontainers.Container] struct ``` #### Signature #####  Key ```go // Key returns the key used to identify the service in the Fiber app's state. // Consumers should use string constants for service keys to ensure consistency // when retrieving services from the Fiber app's state. func (c *ContainerService[T]) Key() string ``` ##### Container ```go // Container returns the Testcontainers container instance, giving full access to the T type methods. // It's useful to access the container's methods, like [testcontainers.Container.MappedPort] // or [testcontainers.Container.Inspect]. func (c *ContainerService[T]) Container() T ``` ##### Start ```go // Start creates and starts the container, calling the [run] function with the [img] and [opts] arguments. // It implements the [fiber.Service] interface. func (c *ContainerService[T]) Start(ctx context.Context) error ``` ##### String ```go // String returns the service key, which uniquely identifies the container service. // It implements the [fiber.Service] interface. func (c *ContainerService[T]) String() string ``` ##### State ```go // State returns the status of the container. // It implements the [fiber.Service] interface. func (c *ContainerService[T]) State(ctx context.Context) (string, error) ``` ##### Terminate ```go // Terminate stops and removes the container. It implements the [fiber.Service] interface. func (c *ContainerService[T]) Terminate(ctx context.Context) error ``` ### Common Errors | Error | Description | Resolution | |-------|-------------|------------| | ErrNilConfig | Returned when the config is nil | Ensure config is properly initialized | | ErrContainerNotRunning | Returned when the container is not running | Check container state before operations | | ErrEmptyServiceKey | Returned when the service key is empty | Provide a non-empty service key | | ErrImageEmpty | Returned when the image is empty | Provide a valid image name | | ErrRunNil | Returned when the run is nil | Provide a valid run function | ## Examples You can find more examples in the [testable examples](https://github.com/gofiber/contrib/blob/main/v3/testcontainers/examples_test.go). ### Adding a module container using the Testcontainers Go's Redis module ```go package main import ( "fmt" "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/testcontainers" tc "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/redis" ) func main() { cfg := &fiber.Config{} // Define the base key for the module service. // The service returned by the [testcontainers.AddService] function, // using the [ContainerService.Key] method, // concatenates the base key with the "using testcontainers-go" suffix. const ( redisKey = "redis-module" ) // Adding containers coming from the testcontainers-go modules, // in this case, a Redis and a Postgres container. redisModuleConfig := testcontainers.NewModuleConfig(redisKey, "redis:latest", redis.Run) redisSrv, err := testcontainers.AddService(cfg, redisModuleConfig) if err != nil { log.Println("error adding redis module:", err) return } // Create a new Fiber app, using the provided configuration. app := fiber.New(*cfg) // Retrieve all services from the app's state. // This returns a slice of all the services registered in the app's state. srvs := app.State().Services() // Retrieve the Redis container from the app's state using the key returned by the [ContainerService.Key] method. redisCtr := fiber.MustGetService[*testcontainers.ContainerService[*redis.RedisContainer]](app.State(), redisSrv.Key()) // Start the Fiber app. app.Listen(":3000") } ``` ### Adding a custom container using the Testcontainers Go package ```go package main import ( "fmt" "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/testcontainers" tc "github.com/testcontainers/testcontainers-go" ) func main() { cfg := &fiber.Config{} // Define the base key for the generic service. // The service returned by the [testcontainers.AddService] function, // using the [ContainerService.Key] method, // concatenates the base key with the "using testcontainers-go" suffix. const ( nginxKey = "nginx-generic" ) // Adding a generic container, directly from the testcontainers-go package. containerConfig := testcontainers.NewContainerConfig(nginxKey, "nginx:latest", tc.WithExposedPorts("80/tcp")) nginxSrv, err := testcontainers.AddService(cfg, containerConfig) if err != nil { log.Println("error adding nginx generic:", err) return } app := fiber.New(*cfg) nginxCtr := fiber.MustGetService[*testcontainers.ContainerService[*tc.DockerContainer]](app.State(), nginxSrv.Key()) // Start the Fiber app. app.Listen(":3000") } ``` ================================================ FILE: v3/testcontainers/config.go ================================================ package testcontainers import ( "context" tc "github.com/testcontainers/testcontainers-go" ) // Config contains the configuration for a container service. type Config[T tc.Container] struct { // ServiceKey is the key used to identify the service in the Fiber app's state. ServiceKey string // Image is the image name to use for the container. Image string // Run is 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, // although it could be the generic [tc.Run] function from the testcontainers-go package. Run func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (T, error) // Options are the functional options to pass to the [tc.Run] function. This argument is optional. // You can find the available options in the [testcontainers website]. // // [testcontainers website]: https://golang.tc.org/features/creating_container/#customizing-the-container Options []tc.ContainerCustomizer } // NewModuleConfig creates a new container service config for a module. // // - The serviceKey is the key used to identify the service in the Fiber app's state. // - The img is the image name to use for the container. // - 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]. // - The opts are the functional options to pass to the run function. This argument is optional. func NewModuleConfig[T tc.Container]( serviceKey string, img string, run func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (T, error), opts ...tc.ContainerCustomizer, ) Config[T] { return Config[T]{ ServiceKey: serviceKey, Image: img, Run: run, Options: opts, } } // NewContainerConfig creates a new container service config for a generic container type, // not created by a Testcontainers module. So this function best used in combination with // the [AddService] function to add a custom container to the Fiber app's state. // // - The serviceKey is the key used to identify the service in the Fiber app's state. // - The img is the image name to use for the container. // - The opts are the functional options to pass to the [tc.Run] function. This argument is optional. // // This function uses the [tc.Run] function as the run function. func NewContainerConfig(serviceKey string, img string, opts ...tc.ContainerCustomizer) Config[*tc.DockerContainer] { return NewModuleConfig(serviceKey, img, tc.Run, opts...) } ================================================ FILE: v3/testcontainers/examples_test.go ================================================ package testcontainers_test import ( "fmt" "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/testcontainers" tc "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/postgres" "github.com/testcontainers/testcontainers-go/modules/redis" ) func ExampleAddService_fromContainer() { cfg := &fiber.Config{} // Define the base key for the generic service. // The service returned by the [testcontainers.AddService] function, // using the [ContainerService.Key] method, // concatenates the base key with the "using testcontainers-go" suffix. const ( nginxKey = "nginx-generic" ) // Adding a generic container, directly from the testcontainers-go package. containerConfig := testcontainers.NewContainerConfig(nginxKey, "nginx:latest", tc.WithExposedPorts("80/tcp")) nginxSrv, err := testcontainers.AddService(cfg, containerConfig) if err != nil { log.Println("error adding nginx generic:", err) return } app := fiber.New(*cfg) fmt.Println(app.State().ServicesLen()) srvs := app.State().Services() fmt.Println(len(srvs)) nginxCtr := fiber.MustGetService[*testcontainers.ContainerService[*tc.DockerContainer]](app.State(), nginxSrv.Key()) fmt.Println(nginxCtr.String()) // Output: // 1 // 1 // nginx-generic (using testcontainers-go) } func ExampleAddService_fromModule() { cfg := &fiber.Config{} // Define the base keys for the module services. // The service returned by the [testcontainers.AddService] function, // using the [ContainerService.Key] method, // concatenates the base key with the "using testcontainers-go" suffix. const ( redisKey = "redis-module" postgresKey = "postgres-module" ) // Adding containers coming from the testcontainers-go modules, // in this case, a Redis and a Postgres container. redisModuleConfig := testcontainers.NewModuleConfig(redisKey, "redis:latest", redis.Run) redisSrv, err := testcontainers.AddService(cfg, redisModuleConfig) if err != nil { log.Println("error adding redis module:", err) return } postgresModuleConfig := testcontainers.NewModuleConfig(postgresKey, "postgres:latest", postgres.Run) postgresSrv, err := testcontainers.AddService(cfg, postgresModuleConfig) if err != nil { log.Println("error adding postgres module:", err) return } // Create a new Fiber app, using the provided configuration. app := fiber.New(*cfg) // Verify the number of services in the app's state. fmt.Println(app.State().ServicesLen()) // Retrieve all services from the app's state. // This returns a slice of all the services registered in the app's state. srvs := app.State().Services() fmt.Println(len(srvs)) // Retrieve the Redis container from the app's state using the key returned by the [ContainerService.Key] method. redisCtr := fiber.MustGetService[*testcontainers.ContainerService[*redis.RedisContainer]](app.State(), redisSrv.Key()) // Retrieve the Postgres container from the app's state using the key returned by the [ContainerService.Key] method. postgresCtr := fiber.MustGetService[*testcontainers.ContainerService[*postgres.PostgresContainer]](app.State(), postgresSrv.Key()) // Verify the string representation of the Redis and Postgres containers. fmt.Println(redisCtr.String()) fmt.Println(postgresCtr.String()) // Output: // 2 // 2 // redis-module (using testcontainers-go) // postgres-module (using testcontainers-go) } ================================================ FILE: v3/testcontainers/go.mod ================================================ module github.com/gofiber/contrib/v3/testcontainers go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/testcontainers/testcontainers-go v0.42.0 ) // Test-time dependencies require ( github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 ) require ( dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v1.0.0-rc.4 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/mdelapenya/tlscert v0.2.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect github.com/moby/moby/api v1.54.1 // indirect github.com/moby/moby/client v0.4.0 // indirect github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/testcontainers/go.sum ================================================ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 h1:id/6LH8ZeDrtAUVSuNvZUAJ1kVpb82y1pr9yweAWsRg= github.com/testcontainers/testcontainers-go/modules/redis v0.42.0/go.mod h1:uF0jI8FITagQpBNOgweGBmPf6rP4K0SeL1XFPbsZSSY= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= ================================================ FILE: v3/testcontainers/testcontainers.go ================================================ package testcontainers import ( "context" "errors" "fmt" "strings" "github.com/gofiber/fiber/v3" tc "github.com/testcontainers/testcontainers-go" ) const ( // serviceSuffix is the suffix added to the service key to identify it as a Testcontainers service. serviceSuffix = " (using testcontainers-go)" // fiberContainerLabel is the label added to the container to identify it as a Fiber app. fiberContainerLabel = "org.testcontainers.golang.framework" // fiberContainerLabelValue is the value of the label added to the container to identify it as a Fiber app. fiberContainerLabelValue = "Go Fiber" ) var ( // ErrNilConfig is returned when the config is nil. ErrNilConfig = errors.New("config is nil") // ErrContainerNotRunning is returned when the container is not running. ErrContainerNotRunning = errors.New("container is not running") // ErrEmptyServiceKey is returned when the service key is empty. ErrEmptyServiceKey = errors.New("service key is empty") // ErrImageEmpty is returned when the image is empty. ErrImageEmpty = errors.New("image is empty") // ErrRunNil is returned when the run is nil. ErrRunNil = errors.New("run is nil") ) // buildKey builds a key for a container service. // This key is used to identify the service in the Fiber app's state. func buildKey(key string) string { if strings.HasSuffix(key, serviceSuffix) { return key } return key + serviceSuffix } // ContainerService represents a container that implements the [fiber.Service] interface. // It manages the lifecycle of a [tc.Container] instance, and it can be // retrieved from the Fiber app's state calling the [fiber.MustGetService] function with // the key returned by the [ContainerService.Key] method. // // The type parameter T must implement the [tc.Container] interface. type ContainerService[T tc.Container] struct { // The container instance, using the generic type T. ctr T // initialized tracks whether the container has been started initialized bool // The key used to identify the service in the Fiber app's state. key string // The image to use for the container. // It's used to run the container with a specific image. img string // The functional options to pass to the [run] function. // It's used to customize the container. opts []tc.ContainerCustomizer // The function to use to run the container. // It's usually the Run function from a testcontainers-go module, like redis.Run or postgres.Run, // or the Run function from the testcontainers-go package. // It returns a container instance of type T, which embeds [tc.Container], // like [redis.RedisContainer] or [postgres.PostgresContainer]. run func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (T, error) } // Key returns the key used to identify the service in the Fiber app's state. // Consumers should use string constants for service keys to ensure consistency // when retrieving services from the Fiber app's state. func (c *ContainerService[T]) Key() string { return c.key } // Container returns the Testcontainers container instance, giving full access to the T type methods. // It's useful to access the container's methods, like [tc.Container.MappedPort] // or [tc.Container.Inspect]. func (c *ContainerService[T]) Container() T { if !c.initialized { var zero T return zero } return c.ctr } // Start creates and starts the container, calling the [run] function with the [img] and [opts] arguments. // It implements the [fiber.Service] interface. func (c *ContainerService[T]) Start(ctx context.Context) error { if c.initialized { return fmt.Errorf("container %s already initialized", c.key) } opts := append([]tc.ContainerCustomizer{}, c.opts...) opts = append(opts, tc.WithLabels(map[string]string{ fiberContainerLabel: fiberContainerLabelValue, })) ctr, err := c.run(ctx, c.img, opts...) if err != nil { return fmt.Errorf("run container: %w", err) } c.ctr = ctr c.initialized = true return nil } // String returns the service key, which uniquely identifies the container service. // It implements the [fiber.Service] interface. func (c *ContainerService[T]) String() string { return c.key } // State returns the status of the container. // It implements the [fiber.Service] interface. func (c *ContainerService[T]) State(ctx context.Context) (string, error) { if !c.initialized { return "", ErrContainerNotRunning } st, err := c.ctr.State(ctx) if err != nil { return "", fmt.Errorf("get container state for %s: %w", c.key, err) } if st == nil { return "", fmt.Errorf("container state is nil for %s", c.key) } return string(st.Status), nil } // Terminate stops and removes the container. It implements the [fiber.Service] interface. func (c *ContainerService[T]) Terminate(ctx context.Context) error { if !c.initialized { return ErrContainerNotRunning } if err := c.ctr.Terminate(ctx); err != nil { return fmt.Errorf("terminate container: %w", err) } c.initialized = false // Reset container reference to avoid potential use after free var zero T c.ctr = zero return nil } // AddService adds a Testcontainers container as a [fiber.Service] for the Fiber app. // It returns a pointer to a [ContainerService[T]] object, which contains the key used to identify // the service in the Fiber app's state, and an error if the config is nil. // The container should be a function like redis.Run or postgres.Run that returns a container type // which embeds [tc.Container]. // - The cfg is the Fiber app's configuration, needed to add the service to the Fiber app's state. // - The containerConfig is the configuration for the container, where: // - The containerConfig.ServiceKey is the key used to identify the service in the Fiber app's state. // - 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. // - The containerConfig.Image is the image to use for the container. // - The containerConfig.Options are the functional options to pass to the [tc.Run] function. This argument is optional. // // Use [NewModuleConfig] or [NewContainerConfig] helper functions to create valid containerConfig objects. func AddService[T tc.Container](cfg *fiber.Config, containerConfig Config[T]) (*ContainerService[T], error) { if cfg == nil { return nil, ErrNilConfig } if containerConfig.ServiceKey == "" { return nil, ErrEmptyServiceKey } if containerConfig.Image == "" { return nil, ErrImageEmpty } if containerConfig.Run == nil { return nil, ErrRunNil } k := buildKey(containerConfig.ServiceKey) c := &ContainerService[T]{ key: k, img: containerConfig.Image, opts: containerConfig.Options, run: containerConfig.Run, } cfg.Services = append(cfg.Services, c) return c, nil } ================================================ FILE: v3/testcontainers/testcontainers_test.go ================================================ package testcontainers_test import ( "context" "testing" "time" "github.com/gofiber/contrib/v3/testcontainers" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" tc "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/redis" "github.com/testcontainers/testcontainers-go/wait" ) const ( nginxAlpineImg = "nginx:alpine" redisAlpineImg = "redis:alpine" postgresAlpineImg = "postgres:alpine" ) func TestAddService_fromContainerConfig(t *testing.T) { t.Run("nil-config", func(t *testing.T) { containerConfig := testcontainers.NewContainerConfig("nginx-generic", nginxAlpineImg) srv, err := testcontainers.AddService(nil, containerConfig) require.ErrorIs(t, err, testcontainers.ErrNilConfig) require.Nil(t, srv) }) t.Run("empty-service-key", func(t *testing.T) { containerConfig := testcontainers.NewContainerConfig("", nginxAlpineImg) srv, err := testcontainers.AddService(&fiber.Config{}, containerConfig) require.ErrorIs(t, err, testcontainers.ErrEmptyServiceKey) require.Nil(t, srv) }) t.Run("empty-image", func(t *testing.T) { containerConfig := testcontainers.NewContainerConfig("nginx-generic", "") srv, err := testcontainers.AddService(&fiber.Config{}, containerConfig) require.ErrorIs(t, err, testcontainers.ErrImageEmpty) require.Nil(t, srv) }) t.Run("success", func(t *testing.T) { cfg := fiber.Config{} containerConfig := testcontainers.NewContainerConfig("nginx-generic", nginxAlpineImg, tc.WithExposedPorts("80/tcp")) srv, err := testcontainers.AddService(&cfg, containerConfig) require.NoError(t, err) require.Equal(t, "nginx-generic (using testcontainers-go)", srv.Key()) app := fiber.New(cfg) require.Len(t, app.State().Services(), 1) require.Equal(t, 1, app.State().ServicesLen()) }) } func TestAddService_fromModuleConfig(t *testing.T) { t.Run("nil-fiber-config", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(nil, moduleConfig) require.ErrorIs(t, err, testcontainers.ErrNilConfig) require.Nil(t, srv) }) t.Run("empty-service-key", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&fiber.Config{}, moduleConfig) require.ErrorIs(t, err, testcontainers.ErrEmptyServiceKey) require.Nil(t, srv) }) t.Run("empty-image", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module", "", redis.Run) srv, err := testcontainers.AddService(&fiber.Config{}, moduleConfig) require.ErrorIs(t, err, testcontainers.ErrImageEmpty) require.Nil(t, srv) }) t.Run("nil-run-fn", func(t *testing.T) { var run func(ctx context.Context, img string, opts ...tc.ContainerCustomizer) (tc.Container, error) moduleConfig := testcontainers.NewModuleConfig("redis-module", redisAlpineImg, run) srv, err := testcontainers.AddService(&fiber.Config{}, moduleConfig) require.ErrorIs(t, err, testcontainers.ErrRunNil) require.Nil(t, srv) }) t.Run("add-modules", func(t *testing.T) { cfg := fiber.Config{} moduleConfig := testcontainers.NewModuleConfig("redis-module", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.Equal(t, "redis-module (using testcontainers-go)", srv.Key()) app := fiber.New(cfg) require.Len(t, app.State().Services(), 1) require.Equal(t, 1, app.State().ServicesLen()) }) } func TestContainerService(t *testing.T) { t.Run("start", func(t *testing.T) { cfg := fiber.Config{} t.Run("success", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.NoError(t, srv.Start(context.Background())) t.Cleanup(func() { require.NoError(t, srv.Terminate(context.Background())) }) ctr := srv.Container() require.NotNil(t, ctr) st, err := srv.State(context.Background()) require.NoError(t, err) require.Equal(t, "running", st) // verify the container has the correct labels inspect, err := srv.Container().Inspect(context.Background()) require.NoError(t, err) require.Equal(t, "Go Fiber", inspect.Config.Labels["org.testcontainers.golang.framework"]) }) t.Run("error", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module-error", redisAlpineImg, redis.Run, tc.WithWaitStrategy(wait.ForLog("never happens").WithStartupTimeout(time.Second))) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.Error(t, srv.Start(context.Background())) ctr := srv.Container() require.Nil(t, ctr) }) t.Run("twice-error", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module-twice-error", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.NoError(t, srv.Start(context.Background())) t.Cleanup(func() { require.NoError(t, srv.Terminate(context.Background())) }) require.Error(t, srv.Start(context.Background())) }) }) t.Run("state", func(t *testing.T) { cfg := fiber.Config{} t.Run("running", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module-running", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.Equal(t, "redis-module-running (using testcontainers-go)", srv.String()) require.NoError(t, srv.Start(context.Background())) t.Cleanup(func() { require.NoError(t, srv.Terminate(context.Background())) }) st, err := srv.State(context.Background()) require.NoError(t, err) require.Equal(t, "running", st) }) t.Run("not-running", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module-not-running", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.Equal(t, "redis-module-not-running (using testcontainers-go)", srv.String()) st, err := srv.State(context.Background()) require.ErrorIs(t, err, testcontainers.ErrContainerNotRunning) require.Empty(t, st) }) }) t.Run("terminate", func(t *testing.T) { cfg := fiber.Config{} t.Run("running", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) // Start the service to be able to terminate it. require.NoError(t, srv.Start(context.Background())) require.NoError(t, srv.Terminate(context.Background())) // The container is terminated, so the state should not be available. _, err = srv.State(context.Background()) require.Error(t, err) }) t.Run("not-running", func(t *testing.T) { moduleConfig := testcontainers.NewModuleConfig("redis-module-not-running", redisAlpineImg, redis.Run) srv, err := testcontainers.AddService(&cfg, moduleConfig) require.NoError(t, err) require.Equal(t, "redis-module-not-running (using testcontainers-go)", srv.String()) err = srv.Terminate(context.Background()) require.ErrorIs(t, err, testcontainers.ErrContainerNotRunning) }) }) } ================================================ FILE: v3/testcontainers/testcontainers_unit_test.go ================================================ package testcontainers import ( "context" "testing" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go/modules/redis" ) func Test_buildKey(t *testing.T) { t.Run("no-suffix", func(t *testing.T) { key := "test" got := buildKey(key) require.Equal(t, key+serviceSuffix, got) }) t.Run("with-suffix", func(t *testing.T) { key := "test-suffix" + serviceSuffix got := buildKey(key) require.Equal(t, key, got) }) } func Test_ContainersService_Start(t *testing.T) { t.Run("twice-error", func(t *testing.T) { cfg := fiber.Config{} moduleConfig := NewModuleConfig("redis-module-twice-error", "redis:alpine", redis.Run) srv, err := AddService(&cfg, moduleConfig) require.NoError(t, err) opts1 := srv.opts require.NoError(t, srv.Start(context.Background())) t.Cleanup(func() { require.NoError(t, srv.Terminate(context.Background())) }) require.Error(t, srv.Start(context.Background())) // verify that the opts are not modified opts2 := srv.opts require.Equal(t, opts1, opts2) }) } ================================================ FILE: v3/websocket/README.md ================================================ --- id: websocket --- # Websocket ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*websocket*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20websocket/badge.svg) Based 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). **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/websocket ``` ## Signatures ```go func New(handler func(*websocket.Conn), config ...websocket.Config) fiber.Handler { ``` ## Config | Property | Type | Description | Default | |:--------------------|:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------|:-----------------------| | Next | `func(fiber.Ctx) bool` | Defines a function to skip this middleware when it returns true. | `nil` | | HandshakeTimeout | `time.Duration` | HandshakeTimeout specifies the duration for the handshake to complete. | `0` (No timeout) | | Subprotocols | `[]string` | Subprotocols specifies the client's requested subprotocols. | `nil` | | Origins | `[]string` | Allowed Origins based on the Origin header. If empty, everything is allowed. | `nil` | | AllowEmptyOrigin | `bool` | Allows connections without an Origin header when Origins is configured. Useful for non-browser clients. | `false` | | ReadBufferSize | `int` | ReadBufferSize specifies the I/O buffer size in bytes for incoming messages. | `0` (Use default size) | | WriteBufferSize | `int` | WriteBufferSize specifies the I/O buffer size in bytes for outgoing messages. | `0` (Use default size) | | WriteBufferPool | `websocket.BufferPool` | WriteBufferPool is a pool of buffers for write operations. | `nil` | | EnableCompression | `bool` | EnableCompression specifies if the client should attempt to negotiate per message compression (RFC 7692). | `false` | | RecoverHandler | `func(*websocket.Conn)` | RecoverHandler is a panic handler function that recovers from panics. | `defaultRecover` | ## Example ```go package main import ( "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/contrib/v3/websocket" ) func main() { app := fiber.New() app.Use("/ws", func(c fiber.Ctx) error { // IsWebSocketUpgrade returns true if the client // requested upgrade to the WebSocket protocol. if websocket.IsWebSocketUpgrade(c) { c.Locals("allowed", true) return c.Next() } return fiber.ErrUpgradeRequired }) app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) { // c.Locals is added to the *websocket.Conn log.Println(c.Locals("allowed")) // true log.Println(c.Params("id")) // 123 log.Println(c.Query("v")) // 1.0 log.Println(c.Cookies("session")) // "" // websocket.Conn bindings https://pkg.go.dev/github.com/fasthttp/websocket?tab=doc#pkg-index var ( mt int msg []byte err error ) for { if mt, msg, err = c.ReadMessage(); err != nil { log.Println("read:", err) break } log.Printf("recv: %s", msg) if err = c.WriteMessage(mt, msg); err != nil { log.Println("write:", err) break } } })) log.Fatal(app.Listen(":3000")) // Access the websocket server: ws://localhost:3000/ws/123?v=1.0 // https://www.websocket.org/echo.html } ``` ## Note with cache middleware If 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. ```go app := fiber.New() app.Use(cache.New(cache.Config{ Next: func(c fiber.Ctx) bool { return strings.Contains(c.Route().Path, "/ws") }, })) app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) {})) ``` ## Note with recover middleware For internal implementation reasons, currently recover middleware does not work with websocket middleware, please use `config.RecoverHandler` to add recover handler to websocket endpoints. By default, config `RecoverHandler` recovers from panic and writes stack trace to stderr, also returns a response that contains panic message in **error** field. ```go app := fiber.New() app.Use(cache.New(cache.Config{ Next: func(c fiber.Ctx) bool { return strings.Contains(c.Route().Path, "/ws") }, })) cfg := Config{ RecoverHandler: func(conn *Conn) { if err := recover(); err != nil { conn.WriteJSON(fiber.Map{"customError": "error occurred"}) } }, } app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) {}, cfg)) ``` ## Note for WebSocket subprotocols The 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) . All 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()`. If 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. ================================================ FILE: v3/websocket/go.mod ================================================ module github.com/gofiber/contrib/v3/websocket go 1.25.0 require ( github.com/fasthttp/websocket v1.5.12 github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/stretchr/testify v1.11.1 github.com/valyala/fasthttp v1.70.0 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/websocket/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE= github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/websocket/websocket.go ================================================ // 🚀 Fiber is an Express inspired web framework written in Go with 💖 // 📌 API Documentation: https://fiber.wiki // 📝 Github Repository: https://github.com/gofiber/fiber package websocket import ( "errors" "fmt" "io" "os" "runtime/debug" "sync" "time" "github.com/fasthttp/websocket" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" ) // Config ... type Config struct { // Next defines a function to skip this middleware when it returns true. // Optional. Default: nil Next func(fiber.Ctx) bool // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration // Subprotocols specifies the client's requested subprotocols. Subprotocols []string // Allowed Origin's based on the Origin header, this validate the request origin to // prevent cross-site request forgery. Everything is allowed if left empty. Origins []string // AllowEmptyOrigin allows WebSocket connections when the Origin header is absent. // When false (default), connections without an Origin header are rejected unless Origins includes "*". // Set to true to allow connections from non-browser clients that don't send Origin headers. // Optional. Default: false AllowEmptyOrigin bool // ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer // size is zero, then a useful default size is used. The I/O buffer sizes // do not limit the size of the messages that can be sent or received. ReadBufferSize, WriteBufferSize int // WriteBufferPool is a pool of buffers for write operations. If the value // is not set, then write buffers are allocated to the connection for the // lifetime of the connection. // // A pool is most useful when the application has a modest volume of writes // across a large number of connections. // // Applications should use a single pool for each unique value of // WriteBufferSize. WriteBufferPool websocket.BufferPool // EnableCompression specifies if the client should attempt to negotiate // per message compression (RFC 7692). Setting this value to true does not // guarantee that compression will be supported. Currently only "no context // takeover" modes are supported. EnableCompression bool // RecoverHandler is a panic handler function that recovers from panics // Default recover function is used when nil and writes error message in a response field `error` // It prints stack trace to the stderr by default // Optional. Default: defaultRecover RecoverHandler func(*Conn) } func defaultRecover(c *Conn) { if err := recover(); err != nil { _, _ = fmt.Fprintf(os.Stderr, "panic: %v\n%s\n", err, debug.Stack()) //nolint:errcheck // This will never fail if err := c.WriteJSON(fiber.Map{"error": err}); err != nil { _, _ = fmt.Fprintf(os.Stderr, "could not write error response: %v\n", err) } } } // New returns a new `handler func(*Conn)` that upgrades a client to the // websocket protocol, you can pass an optional config. func New(handler func(*Conn), config ...Config) fiber.Handler { // Init config var cfg Config if len(config) > 0 { cfg = config[0] } if len(cfg.Origins) == 0 { cfg.Origins = []string{"*"} } // Check if wildcard is present in the Origins list during initialization hasWildcard := false for _, origin := range cfg.Origins { if origin == "*" { hasWildcard = true break } } if cfg.ReadBufferSize == 0 { cfg.ReadBufferSize = 1024 } if cfg.WriteBufferSize == 0 { cfg.WriteBufferSize = 1024 } if cfg.RecoverHandler == nil { cfg.RecoverHandler = defaultRecover } var upgrader = websocket.FastHTTPUpgrader{ HandshakeTimeout: cfg.HandshakeTimeout, Subprotocols: cfg.Subprotocols, ReadBufferSize: cfg.ReadBufferSize, WriteBufferSize: cfg.WriteBufferSize, EnableCompression: cfg.EnableCompression, WriteBufferPool: cfg.WriteBufferPool, CheckOrigin: func(fctx *fasthttp.RequestCtx) bool { // Fast path: if Origins is just wildcard (the default), allow all without checking header if len(cfg.Origins) == 1 && cfg.Origins[0] == "*" { return true } origin := utils.UnsafeString(fctx.Request.Header.Peek("Origin")) if origin == "" { // Allow empty Origin if wildcard is in list or explicitly configured return hasWildcard || cfg.AllowEmptyOrigin } // If wildcard is present, allow any non-empty origin if hasWildcard { return true } // No wildcard present, check if origin matches any specific origin in the list for i := range cfg.Origins { if cfg.Origins[i] == origin { return true } } return false }, } return func(c fiber.Ctx) error { if cfg.Next != nil && cfg.Next(c) { return c.Next() } if !c.App().Server().KeepHijackedConns { c.App().Server().KeepHijackedConns = true } conn := acquireConn() // locals c.RequestCtx().VisitUserValues(func(key []byte, value interface{}) { conn.locals[string(key)] = value }) // params params := c.Route().Params for i := 0; i < len(params); i++ { conn.params[utils.CopyString(params[i])] = utils.CopyString(c.Params(params[i])) } // queries queries := c.RequestCtx().QueryArgs().All() for key, value := range queries { conn.queries[string(key)] = string(value) } // cookies cookies := c.RequestCtx().Request.Header.Cookies() for key, value := range cookies { conn.cookies[string(key)] = string(value) } // headers headers := c.RequestCtx().Request.Header.All() for key, value := range headers { conn.headers[string(key)] = string(value) } // ip address conn.ip = utils.CopyString(c.IP()) if err := upgrader.Upgrade(c.RequestCtx(), func(fconn *websocket.Conn) { conn.Conn = fconn defer releaseConn(conn) defer cfg.RecoverHandler(conn) handler(conn) }); err != nil { // Upgrading required releaseConn(conn) return fiber.ErrUpgradeRequired } return nil } } // Conn https://godoc.org/github.com/gorilla/websocket#pkg-index type Conn struct { *websocket.Conn locals map[string]interface{} params map[string]string cookies map[string]string headers map[string]string queries map[string]string ip string } // Conn pool var poolConn = sync.Pool{ New: func() interface{} { return new(Conn) }, } // Acquire Conn from pool func acquireConn() *Conn { conn := poolConn.Get().(*Conn) conn.locals = make(map[string]interface{}) conn.params = make(map[string]string) conn.queries = make(map[string]string) conn.cookies = make(map[string]string) conn.headers = make(map[string]string) return conn } // Return Conn to pool func releaseConn(conn *Conn) { conn.Conn = nil poolConn.Put(conn) } // Locals makes it possible to pass interface{} values under string keys scoped to the request // and therefore available to all following routes that match the request. func (conn *Conn) Locals(key string, value ...interface{}) interface{} { if len(value) == 0 { return conn.locals[key] } conn.locals[key] = value[0] return value[0] } // Params is used to get the route parameters. // Defaults to empty string "" if the param doesn't exist. // If a default value is given, it will return that value if the param doesn't exist. func (conn *Conn) Params(key string, defaultValue ...string) string { v, ok := conn.params[key] if !ok && len(defaultValue) > 0 { return defaultValue[0] } return v } // Query returns the query string parameter in the url. // Defaults to empty string "" if the query doesn't exist. // If a default value is given, it will return that value if the query doesn't exist. func (conn *Conn) Query(key string, defaultValue ...string) string { v, ok := conn.queries[key] if !ok && len(defaultValue) > 0 { return defaultValue[0] } return v } // Cookies is used for getting a cookie value by key // Defaults to empty string "" if the cookie doesn't exist. // If a default value is given, it will return that value if the cookie doesn't exist. func (conn *Conn) Cookies(key string, defaultValue ...string) string { v, ok := conn.cookies[key] if !ok && len(defaultValue) > 0 { return defaultValue[0] } return v } // Headers is used for getting a header value by key // Defaults to empty string "" if the header doesn't exist. // If a default value is given, it will return that value if the header doesn't exist. // Header lookups are case-insensitive. func (conn *Conn) Headers(key string, defaultValue ...string) string { for k, v := range conn.headers { if utils.EqualFold(k, key) { return v } } if len(defaultValue) > 0 { return defaultValue[0] } return "" } // IP returns the client's network address func (conn *Conn) IP() string { return conn.ip } // Constants are taken from https://github.com/fasthttp/websocket/blob/master/conn.go#L43 // Close codes defined in RFC 6455, section 11.7. const ( CloseNormalClosure = 1000 CloseGoingAway = 1001 CloseProtocolError = 1002 CloseUnsupportedData = 1003 CloseNoStatusReceived = 1005 CloseAbnormalClosure = 1006 CloseInvalidFramePayloadData = 1007 ClosePolicyViolation = 1008 CloseMessageTooBig = 1009 CloseMandatoryExtension = 1010 CloseInternalServerErr = 1011 CloseServiceRestart = 1012 CloseTryAgainLater = 1013 CloseTLSHandshake = 1015 ) // The message types are defined in RFC 6455, section 11.8. const ( // TextMessage denotes a text data message. The text message payload is // interpreted as UTF-8 encoded text data. TextMessage = 1 // BinaryMessage denotes a binary data message. BinaryMessage = 2 // CloseMessage denotes a close control message. The optional message // payload contains a numeric code and text. Use the FormatCloseMessage // function to format a close message payload. CloseMessage = 8 // PingMessage denotes a ping control message. The optional message payload // is UTF-8 encoded text. PingMessage = 9 // PongMessage denotes a pong control message. The optional message payload // is UTF-8 encoded text. PongMessage = 10 ) var ( // ErrBadHandshake is returned when the server response to opening handshake is // invalid. ErrBadHandshake = errors.New("websocket: bad handshake") // ErrCloseSent is returned when the application writes a message to the // connection after sending a close message. ErrCloseSent = errors.New("websocket: close sent") // ErrReadLimit is returned when reading a message that is larger than the // read limit set for the connection. ErrReadLimit = errors.New("websocket: read limit exceeded") ) // FormatCloseMessage formats closeCode and text as a WebSocket close message. // An empty message is returned for code CloseNoStatusReceived. func FormatCloseMessage(closeCode int, text string) []byte { return websocket.FormatCloseMessage(closeCode, text) } // IsCloseError returns boolean indicating whether the error is a *CloseError // with one of the specified codes. func IsCloseError(err error, codes ...int) bool { return websocket.IsCloseError(err, codes...) } // IsUnexpectedCloseError returns boolean indicating whether the error is a // *CloseError with a code not in the list of expected codes. func IsUnexpectedCloseError(err error, expectedCodes ...int) bool { return websocket.IsUnexpectedCloseError(err, expectedCodes...) } // IsWebSocketUpgrade returns true if the client requested upgrade to the // WebSocket protocol. func IsWebSocketUpgrade(c fiber.Ctx) bool { return websocket.FastHTTPIsWebSocketUpgrade(c.RequestCtx()) } // JoinMessages concatenates received messages to create a single io.Reader. // The string term is appended to each message. The returned reader does not // support concurrent calls to the Read method. func JoinMessages(c *websocket.Conn, term string) io.Reader { return websocket.JoinMessages(c, term) } ================================================ FILE: v3/websocket/websocket_test.go ================================================ package websocket import ( "net" "net/http" "testing" "time" "github.com/fasthttp/websocket" "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWebSocketMiddlewareDefaultConfig(t *testing.T) { app := setupTestApp(Config{}, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketMiddlewareConfigOrigin(t *testing.T) { t.Run("allow all origins", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"*"}, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", http.Header{ "Origin": []string{"http://localhost:3000"}, }) defer conn.Close() assert.NoError(t, err) assert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.Nil(t, err) assert.Equal(t, "hello websocket", msg["message"]) }) t.Run("allowed origin", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"http://localhost:3000"}, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", http.Header{ "Origin": []string{"http://localhost:3000"}, }) defer conn.Close() assert.NoError(t, err) assert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) }) t.Run("empty origin rejected by default", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"http://localhost:3000"}, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) if conn != nil { defer conn.Close() } assert.Error(t, err) assert.Contains(t, err.Error(), "bad handshake") assert.Equal(t, fiber.StatusUpgradeRequired, resp.StatusCode) assert.Equal(t, "", resp.Header.Get("Upgrade")) assert.Nil(t, conn) }) t.Run("empty origin allowed with config", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"http://localhost:3000"}, AllowEmptyOrigin: true, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) }) t.Run("wildcard in list", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"http://localhost:3000", "*"}, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", http.Header{ "Origin": []string{"http://localhost:5000"}, }) if !assert.NoError(t, err) { return } defer conn.Close() assert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) }) t.Run("wildcard in list allows empty origin", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"http://localhost:3000", "*"}, }, nil) defer app.Shutdown() // Explicitly test with no Origin header (nil headers = no Origin sent) conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) if !assert.NoError(t, err) { return } defer conn.Close() assert.Equal(t, fiber.StatusSwitchingProtocols, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) }) t.Run("disallowed origin", func(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"http://localhost:3000"}, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", http.Header{ "Origin": []string{"http://localhost:5000"}, }) defer conn.Close() assert.Equal(t, err.Error(), "websocket: bad handshake") assert.Equal(t, fiber.StatusUpgradeRequired, resp.StatusCode) assert.Equal(t, "", resp.Header.Get("Upgrade")) assert.Nil(t, conn) }) } func TestWebSocketMiddlewareBufferSize(t *testing.T) { app := setupTestApp(Config{ Origins: []string{"*"}, WriteBufferSize: 10, }, nil) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketConnParams(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { param1 := c.Params("param1") param2 := c.Params("param2") paramDefault := c.Params("paramDefault", "default") assert.Equal(t, "value1", param1) assert.Equal(t, "value2", param2) assert.Equal(t, "default", paramDefault) c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message/value1/value2", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketConnQuery(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { query1 := c.Query("query1") query2 := c.Query("query2") queryDefault := c.Query("queryDefault", "default") assert.Equal(t, "value1", query1) assert.Equal(t, "value2", query2) assert.Equal(t, "default", queryDefault) c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message?query1=value1&query2=value2", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketConnHeaders(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { header1 := c.Headers("Header1") header2 := c.Headers("Header2") contentType := c.Headers("Content-Type") headerDefault := c.Headers("HeaderDefault", "valueDefault") assert.Equal(t, "value1", header1) assert.Equal(t, "value2", header2) assert.Equal(t, "application/json", contentType) assert.Equal(t, "valueDefault", headerDefault) c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", http.Header{ "header1": []string{"value1"}, "header2": []string{"value2"}, "content-type": []string{"application/json"}, }) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketConnCookies(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { cookie1 := c.Cookies("Cookie1") cookie2 := c.Cookies("Cookie2") cookieDefault := c.Headers("CookieDefault", "valueDefault") assert.Equal(t, "value1", cookie1) assert.Equal(t, "value2", cookie2) assert.Equal(t, "valueDefault", cookieDefault) c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", http.Header{ "header1": []string{"value1"}, "header2": []string{"value2"}, "Cookie": []string{"Cookie1=value1; Cookie2=value2"}, }) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketConnLocals(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { local1 := c.Locals("local1") local2 := c.Locals("local2") assert.Equal(t, "value1", local1) assert.Equal(t, "value2", local2) c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } func TestWebSocketConnIP(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { ip := c.IP() assert.Equal(t, "127.0.0.1", ip) c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) } // TestWebSocketConnIPSafeCopy verifies that conn.IP() returns a safe copy // that is not corrupted when fasthttp reuses its internal buffer for // subsequent requests. See: gofiber/fiber#4208, gofiber/contrib#1800 func TestWebSocketConnIPSafeCopy(t *testing.T) { const iterations = 5 ips := make(chan string, iterations) app := setupTestApp(Config{}, func(c *Conn) { // Read the IP and send it back; the value must remain "127.0.0.1" // even after fasthttp recycles the underlying request buffer. ips <- c.IP() c.WriteJSON(fiber.Map{"ip": c.IP()}) }) defer app.Shutdown() for i := 0; i < iterations; i++ { conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) require.NoError(t, err) var msg fiber.Map err = conn.ReadJSON(&msg) require.NoError(t, err) assert.Equal(t, "127.0.0.1", msg["ip"]) conn.Close() } close(ips) for ip := range ips { assert.Equal(t, "127.0.0.1", ip, "conn.IP() must be a safe copy, not a reference to recycled fasthttp buffer") } } func TestWebSocketCompressionAfterHandlerReturns(t *testing.T) { writeErr := make(chan error, 1) handlerReturning := make(chan struct{}) app := setupTestApp(Config{ EnableCompression: true, }, func(c *Conn) { defer close(handlerReturning) conn := c.Conn go func() { <-handlerReturning conn.EnableWriteCompression(true) if err := conn.SetCompressionLevel(2); err != nil { writeErr <- err return } writeErr <- conn.WriteJSON(fiber.Map{"message": "hello websocket"}) }() }) defer app.Shutdown() dialer := websocket.Dialer{ EnableCompression: true, } conn, resp, err := dialer.Dial("ws://localhost:3000/ws/message", nil) require.NoError(t, err) defer conn.Close() assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) assert.Contains(t, resp.Header.Get("Sec-WebSocket-Extensions"), "permessage-deflate") var msg fiber.Map err = conn.ReadJSON(&msg) require.NoError(t, err) assert.Equal(t, "hello websocket", msg["message"]) select { case err := <-writeErr: assert.NoError(t, err) case <-time.After(time.Second): t.Fatal("timed out waiting for async compressed write") } } func setupTestApp(cfg Config, h func(c *Conn)) *fiber.App { var handler fiber.Handler if h == nil { handler = New(func(c *Conn) { c.WriteJSON(fiber.Map{ "message": "hello websocket", }) }, cfg) } else { handler = New(h, cfg) } app := fiber.New(fiber.Config{}) app.Use("/ws", func(c fiber.Ctx) error { if IsWebSocketUpgrade(c) { fiber.StoreInContext(c, "allowed", true) fiber.StoreInContext(c, "local1", "value1") fiber.StoreInContext(c, "local2", "value2") return c.Next() } return fiber.ErrUpgradeRequired }) app.Get("/ws/message", handler) app.Get("/ws/message/:param1/:param2", handler) go app.Listen(":3000", fiber.ListenConfig{DisableStartupMessage: true}) readyCh := make(chan struct{}) go func() { for { conn, err := net.Dial("tcp", "localhost:3000") if err != nil { continue } if conn != nil { readyCh <- struct{}{} conn.Close() break } } }() <-readyCh return app } func TestWebSocketIsCloseError(t *testing.T) { closeError := IsCloseError(&websocket.CloseError{ Code: websocket.CloseNormalClosure, }, websocket.CloseNormalClosure) assert.Equal(t, true, closeError) } func TestWebSocketIsUnexpectedCloseError(t *testing.T) { closeError := IsUnexpectedCloseError(&websocket.CloseError{ Code: websocket.CloseNormalClosure, }, websocket.CloseAbnormalClosure) assert.Equal(t, true, closeError) } func TestWebSocketFormatCloseMessage(t *testing.T) { closeMsg := FormatCloseMessage(websocket.CloseNormalClosure, "test") assert.Equal(t, []byte{0x3, 0xe8, 0x74, 0x65, 0x73, 0x74}, closeMsg) } func TestWebsocketRecoverDefaultHandlerShouldNotPanic(t *testing.T) { app := setupTestApp(Config{}, func(c *Conn) { panic("test panic") }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "test panic", msg["error"]) } func TestWebsocketRecoverCustomHandlerShouldNotPanic(t *testing.T) { app := setupTestApp(Config{ RecoverHandler: func(conn *Conn) { if err := recover(); err != nil { conn.WriteJSON(fiber.Map{"customError": "error occurred"}) } }, }, func(c *Conn) { panic("test panic") }) defer app.Shutdown() conn, resp, err := websocket.DefaultDialer.Dial("ws://localhost:3000/ws/message", nil) defer conn.Close() assert.NoError(t, err) assert.Equal(t, 101, resp.StatusCode) assert.Equal(t, "websocket", resp.Header.Get("Upgrade")) var msg fiber.Map err = conn.ReadJSON(&msg) assert.NoError(t, err) assert.Equal(t, "error occurred", msg["customError"]) } ================================================ FILE: v3/zap/.gitignore ================================================ all/ debug/ info/ warn/ error/ *.log ================================================ FILE: v3/zap/README.md ================================================ --- id: zap --- # Zap ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*zap*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20zap/badge.svg) [Zap](https://github.com/uber-go/zap) logging support for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/zap go get -u go.uber.org/zap ``` ### Signature ```go zap.New(config ...zap.Config) fiber.Handler ``` ### Config | Property | Type | Description | Default | | :--------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- | | Next | `func(fiber.Ctx) bool` | Define a function to skip this middleware when returned true | `nil` | | Logger | `*zap.Logger` | Add custom zap logger. | `zap.NewProduction()` | | Fields | `[]string` | Add fields that you want to see. | `[]string{"latency", "status", "method", "url"}` | | FieldsFunc | `func(fiber.Ctx) []zap.Field` | Define a function to add custom fields. | `nil` | | Messages | `[]string` | Custom response messages. | `[]string{"Server error", "Client error", "Success"}` | | Levels | `[]zapcore.Level` | Custom response levels. | `[]zapcore.Level{zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel}` | | SkipURIs | `[]string` | Skip logging these URI. | `[]string{}` | | GetResBody | `func(c fiber.Ctx) []byte` | Define a function to get response body when return non-nil.
eg: When use compress middleware, resBody is unreadable. you can set GetResBody func to get readable resBody. | `nil` | ### Example ```go package main import ( "log" middleware "github.com/gofiber/contrib/v3/zap" "github.com/gofiber/fiber/v3" "go.uber.org/zap" ) func main() { app := fiber.New() logger, _ := zap.NewProduction() defer logger.Sync() app.Use(middleware.New(middleware.Config{ Logger: logger, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") }) log.Fatal(app.Listen(":3000")) } ``` ## NewLogger ### Signature ```go zap.NewLogger(config ...zap.LoggerConfig) *zap.LoggerConfig ``` ### LoggerConfig | Property | Type | Description | Default | | :---------- | :------------- | :------------------------------------------------------------------------------------------------------- | :----------------------------- | | CoreConfigs | `[]CoreConfig` | Define Config for zapcore | `zap.LoggerConfigDefault` | | SetLogger | `*zap.Logger` | Add custom zap logger. if not nil, `ZapOptions`, `CoreConfigs`, `SetLevel`, `SetOutput` will be ignored. | `nil` | | ExtraKeys | `[]string` | Allow users log extra values from context. | `[]string{}` | | ZapOptions | `[]zap.Option` | Allow users to configure the zap.Option supplied by zap. | `[]zap.Option{}` | ### Example ```go package main import ( "context" middleware "github.com/gofiber/contrib/v3/zap" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/log" ) func main() { app := fiber.New() logger := middleware.NewLogger(middleware.LoggerConfig{ ExtraKeys: []string{"request_id"}, }) log.SetLogger(logger) defer logger.Sync() app.Use(func(c fiber.Ctx) error { ctx := context.WithValue(c.Context(), "request_id", "123") c.SetContext(ctx) return c.Next() }) app.Get("/", func(c fiber.Ctx) error { log.WithContext(c.Context()).Info("Hello, World!") return c.SendString("Hello, World!") }) log.Fatal(app.Listen(":3000")) } ``` ================================================ FILE: v3/zap/config.go ================================================ package zap import ( "github.com/gofiber/fiber/v3" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Config defines the config for middleware. type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool // SkipBody defines a function to skip log "body" field when returned true. // // Optional. Default: nil SkipBody func(c fiber.Ctx) bool // SkipResBody defines a function to skip log "resBody" field when returned true. // // Optional. Default: nil SkipResBody func(c fiber.Ctx) bool // GetResBody defines a function to get ResBody. // eg: when use compress middleware, resBody is unreadable. you can set GetResBody func to get readable resBody. // // Optional. Default: nil GetResBody func(c fiber.Ctx) []byte // Skip logging for these uri // // Optional. Default: nil SkipURIs []string // Add custom zap logger. // // Optional. Default: zap.NewProduction() Logger *zap.Logger // Add fields what you want see. // // Optional. Default: {"ip", "latency", "status", "method", "url"} Fields []string // FieldsFunc defines a function to return custom zap fields to append to the log. // // Optional. Default: nil FieldsFunc func(c fiber.Ctx) []zap.Field // Custom response messages. // Response codes >= 500 will be logged with Messages[0]. // Response codes >= 400 will be logged with Messages[1]. // Other response codes will be logged with Messages[2]. // You can specify less, than 3 messages, but you must specify at least 1. // Specifying more than 3 messages is useless. // // Optional. Default: {"Server error", "Client error", "Success"} Messages []string // Custom response levels. // Response codes >= 500 will be logged with Levels[0]. // Response codes >= 400 will be logged with Levels[1]. // Other response codes will be logged with Levels[2]. // You can specify less, than 3 levels, but you must specify at least 1. // Specifying more than 3 levels is useless. // // Optional. Default: {zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel} Levels []zapcore.Level } // Use zap.NewProduction() as default logging instance. var logger, _ = zap.NewProduction() // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, Logger: logger, Fields: []string{"ip", "latency", "status", "method", "url"}, FieldsFunc: nil, Messages: []string{"Server error", "Client error", "Success"}, Levels: []zapcore.Level{zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel}, } // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided if len(config) < 1 { return ConfigDefault } // Override default config cfg := config[0] // Set default values if cfg.Next == nil { cfg.Next = ConfigDefault.Next } if cfg.Logger == nil { cfg.Logger = ConfigDefault.Logger } if cfg.Fields == nil { cfg.Fields = ConfigDefault.Fields } if cfg.Messages == nil { cfg.Messages = ConfigDefault.Messages } if cfg.Levels == nil { cfg.Levels = ConfigDefault.Levels } if cfg.FieldsFunc == nil { cfg.FieldsFunc = ConfigDefault.FieldsFunc } return cfg } ================================================ FILE: v3/zap/go.mod ================================================ module github.com/gofiber/contrib/v3/zap go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/gofiber/utils/v2 v2.0.3 github.com/stretchr/testify v1.11.1 github.com/valyala/fasthttp v1.70.0 go.uber.org/zap v1.27.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/zap/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/zap/logger.go ================================================ package zap import ( "context" "fmt" "io" "os" fiberlog "github.com/gofiber/fiber/v3/log" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var _ fiberlog.AllLogger[*zap.Logger] = (*LoggerConfig)(nil) type LoggerConfig struct { // CoreConfigs allows users to configure Encoder, WriteSyncer, LevelEnabler configuration items provided by zapcore // // Optional. Default: LoggerConfigDefault CoreConfigs []CoreConfig // ZapOptions allow users to configure the zap.Option supplied by zap. // // Optional. Default: []zap.Option ZapOptions []zap.Option // ExtraKeys allow users log extra values from context // // Optional. Default: []string ExtraKeys []string // SetLogger sets *zap.Logger for fiberlog, if set, ZapOptions, CoreConfigs, SetLevel, SetOutput will be ignored // // Optional. Default: nil SetLogger *zap.Logger logger *zap.Logger } // WithContext returns a new LoggerConfig with extra fields from context func (l *LoggerConfig) WithContext(ctx context.Context) fiberlog.CommonLogger { loggerOptions := l.logger.WithOptions(zap.AddCallerSkip(-1)) newLogger := &LoggerConfig{logger: loggerOptions} if len(l.ExtraKeys) > 0 { sugar := l.logger.Sugar() for _, k := range l.ExtraKeys { value := ctx.Value(k) sugar = sugar.With(k, value) } // assign the new sugar to the new LoggerConfig newLogger.logger = sugar.Desugar() } return newLogger } type CoreConfig struct { Encoder zapcore.Encoder WriteSyncer zapcore.WriteSyncer LevelEncoder zapcore.LevelEnabler } // LoggerConfigDefault is the default config var LoggerConfigDefault = LoggerConfig{ CoreConfigs: []CoreConfig{ { Encoder: zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), WriteSyncer: zapcore.AddSync(os.Stdout), LevelEncoder: zap.NewAtomicLevelAt(zap.InfoLevel), }, }, ZapOptions: []zap.Option{ zap.AddCaller(), zap.AddCallerSkip(3), }, } func loggerConfigDefault(config ...LoggerConfig) LoggerConfig { // Return default config if nothing provided if len(config) < 1 { return LoggerConfigDefault } // Override default config cfg := config[0] if cfg.CoreConfigs == nil { cfg.CoreConfigs = LoggerConfigDefault.CoreConfigs } if cfg.SetLogger != nil { cfg.logger = cfg.SetLogger } if cfg.ZapOptions == nil { cfg.ZapOptions = LoggerConfigDefault.ZapOptions } // Remove duplicated extraKeys for _, k := range cfg.ExtraKeys { if !contains(k, cfg.ExtraKeys) { cfg.ExtraKeys = append(cfg.ExtraKeys, k) } } return cfg } // NewLogger creates a new zap logger adapter for fiberlog func NewLogger(config ...LoggerConfig) *LoggerConfig { cfg := loggerConfigDefault(config...) // Return logger if already exists if cfg.SetLogger != nil { return &cfg } zapCores := make([]zapcore.Core, len(cfg.CoreConfigs)) for i, coreConfig := range cfg.CoreConfigs { zapCores[i] = zapcore.NewCore(coreConfig.Encoder, coreConfig.WriteSyncer, coreConfig.LevelEncoder) } core := zapcore.NewTee(zapCores...) logger := zap.New(core, cfg.ZapOptions...) cfg.logger = logger return &cfg } // SetOutput sets the output destination for the logger. func (l *LoggerConfig) SetOutput(w io.Writer) { if l.SetLogger != nil { fiberlog.Warn("SetOutput is ignored when SetLogger is set") return } l.CoreConfigs[0].WriteSyncer = zapcore.AddSync(w) zapCores := make([]zapcore.Core, len(l.CoreConfigs)) for i, coreConfig := range l.CoreConfigs { zapCores[i] = zapcore.NewCore(coreConfig.Encoder, coreConfig.WriteSyncer, coreConfig.LevelEncoder) } core := zapcore.NewTee(zapCores...) logger := zap.New(core, l.ZapOptions...) l.logger = logger } func (l *LoggerConfig) SetLevel(lv fiberlog.Level) { if l.SetLogger != nil { fiberlog.Warn("SetLevel is ignored when SetLogger is set") return } var level zapcore.Level switch lv { case fiberlog.LevelTrace, fiberlog.LevelDebug: level = zap.DebugLevel case fiberlog.LevelInfo: level = zap.InfoLevel case fiberlog.LevelWarn: level = zap.WarnLevel case fiberlog.LevelError: level = zap.ErrorLevel case fiberlog.LevelFatal: level = zap.FatalLevel case fiberlog.LevelPanic: level = zap.PanicLevel default: level = zap.WarnLevel } l.CoreConfigs[0].LevelEncoder = level zapCores := make([]zapcore.Core, len(l.CoreConfigs)) for i, coreConfig := range l.CoreConfigs { zapCores[i] = zapcore.NewCore(coreConfig.Encoder, coreConfig.WriteSyncer, coreConfig.LevelEncoder) } core := zapcore.NewTee(zapCores...) l.logger = zap.New(core, l.ZapOptions...) } func (l *LoggerConfig) Logf(level fiberlog.Level, format string, kvs ...interface{}) { logger := l.logger.Sugar() switch level { case fiberlog.LevelTrace, fiberlog.LevelDebug: logger.Debugf(format, kvs...) case fiberlog.LevelInfo: logger.Infof(format, kvs...) case fiberlog.LevelWarn: logger.Warnf(format, kvs...) case fiberlog.LevelError: logger.Errorf(format, kvs...) case fiberlog.LevelFatal: logger.Fatalf(format, kvs...) default: logger.Warnf(format, kvs...) } } func (l *LoggerConfig) Trace(v ...interface{}) { l.Log(fiberlog.LevelTrace, v...) } func (l *LoggerConfig) Debug(v ...interface{}) { l.Log(fiberlog.LevelDebug, v...) } func (l *LoggerConfig) Info(v ...interface{}) { l.Log(fiberlog.LevelInfo, v...) } func (l *LoggerConfig) Warn(v ...interface{}) { l.Log(fiberlog.LevelWarn, v...) } func (l *LoggerConfig) Error(v ...interface{}) { l.Log(fiberlog.LevelError, v...) } func (l *LoggerConfig) Fatal(v ...interface{}) { l.Log(fiberlog.LevelFatal, v...) } func (l *LoggerConfig) Panic(v ...interface{}) { l.Log(fiberlog.LevelPanic, v...) } func (l *LoggerConfig) Tracef(format string, v ...interface{}) { l.Logf(fiberlog.LevelTrace, format, v...) } func (l *LoggerConfig) Debugf(format string, v ...interface{}) { l.Logf(fiberlog.LevelDebug, format, v...) } func (l *LoggerConfig) Infof(format string, v ...interface{}) { l.Logf(fiberlog.LevelInfo, format, v...) } func (l *LoggerConfig) Warnf(format string, v ...interface{}) { l.Logf(fiberlog.LevelWarn, format, v...) } func (l *LoggerConfig) Errorf(format string, v ...interface{}) { l.Logf(fiberlog.LevelError, format, v...) } func (l *LoggerConfig) Fatalf(format string, v ...interface{}) { l.Logf(fiberlog.LevelFatal, format, v...) } func (l *LoggerConfig) Panicf(format string, v ...interface{}) { l.Logf(fiberlog.LevelPanic, format, v...) } func (l *LoggerConfig) Tracew(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelTrace, msg, keysAndValues...) } func (l *LoggerConfig) Debugw(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelDebug, msg, keysAndValues...) } func (l *LoggerConfig) Infow(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelInfo, msg, keysAndValues...) } func (l *LoggerConfig) Warnw(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelWarn, msg, keysAndValues...) } func (l *LoggerConfig) Errorw(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelError, msg, keysAndValues...) } func (l *LoggerConfig) Fatalw(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelFatal, msg, keysAndValues...) } func (l *LoggerConfig) Panicw(msg string, keysAndValues ...interface{}) { l.Logw(fiberlog.LevelPanic, msg, keysAndValues...) } func (l *LoggerConfig) Log(level fiberlog.Level, kvs ...interface{}) { sugar := l.logger.Sugar() switch level { case fiberlog.LevelTrace, fiberlog.LevelDebug: sugar.Debug(kvs...) case fiberlog.LevelInfo: sugar.Info(kvs...) case fiberlog.LevelWarn: sugar.Warn(kvs...) case fiberlog.LevelError: sugar.Error(kvs...) case fiberlog.LevelFatal: sugar.Fatal(kvs...) case fiberlog.LevelPanic: sugar.Panic(kvs...) default: sugar.Warn(kvs...) } } func (l *LoggerConfig) Logw(level fiberlog.Level, msg string, keyvals ...interface{}) { keylen := len(keyvals) if keylen == 0 || keylen%2 != 0 { l.Logger().Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals)) return } data := make([]zap.Field, 0, (keylen/2)+1) for i := 0; i < keylen; i += 2 { data = append(data, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1])) } switch level { case fiberlog.LevelTrace, fiberlog.LevelDebug: l.Logger().Debug(msg, data...) case fiberlog.LevelInfo: l.Logger().Info(msg, data...) case fiberlog.LevelWarn: l.Logger().Warn(msg, data...) case fiberlog.LevelError: l.Logger().Error(msg, data...) case fiberlog.LevelFatal: l.Logger().Fatal(msg, data...) default: l.Logger().Warn(msg, data...) } } // Sync flushes any buffered log entries. func (l *LoggerConfig) Sync() error { return l.logger.Sync() } // Logger returns the underlying *zap.Logger when not using SetLogger func (l *LoggerConfig) Logger() *zap.Logger { return l.logger } ================================================ FILE: v3/zap/logger_test.go ================================================ package zap import ( "bytes" "context" "encoding/json" "os" "path/filepath" "regexp" "strings" "testing" "github.com/gofiber/fiber/v3/log" "github.com/stretchr/testify/assert" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // testEncoderConfig encoder config for testing, copy from zap func testEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ MessageKey: "msg", LevelKey: "level", NameKey: "name", TimeKey: "ts", CallerKey: "caller", FunctionKey: "func", StacktraceKey: "stacktrace", LineEnding: "\n", EncodeTime: zapcore.EpochTimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } } // humanEncoderConfig copy from zap func humanEncoderConfig() zapcore.EncoderConfig { cfg := testEncoderConfig() cfg.EncodeTime = zapcore.ISO8601TimeEncoder cfg.EncodeLevel = zapcore.CapitalLevelEncoder cfg.EncodeDuration = zapcore.StringDurationEncoder return cfg } func getWriteSyncer(file string) zapcore.WriteSyncer { _, err := os.Stat(file) if os.IsNotExist(err) { _ = os.MkdirAll(filepath.Dir(file), 0o744) } f, _ := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) return zapcore.AddSync(f) } // TestCoreOption test zapcore config option func TestCoreOption(t *testing.T) { buf := new(bytes.Buffer) dynamicLevel := zap.NewAtomicLevel() dynamicLevel.SetLevel(zap.InfoLevel) logger := NewLogger( LoggerConfig{ CoreConfigs: []CoreConfig{ { Encoder: zapcore.NewConsoleEncoder(humanEncoderConfig()), WriteSyncer: zapcore.AddSync(os.Stdout), LevelEncoder: dynamicLevel, }, { Encoder: zapcore.NewJSONEncoder(humanEncoderConfig()), WriteSyncer: getWriteSyncer("./all/log.log"), LevelEncoder: zap.NewAtomicLevelAt(zapcore.DebugLevel), }, { Encoder: zapcore.NewJSONEncoder(humanEncoderConfig()), WriteSyncer: getWriteSyncer("./debug/log.log"), LevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev == zap.DebugLevel }), }, { Encoder: zapcore.NewJSONEncoder(humanEncoderConfig()), WriteSyncer: getWriteSyncer("./info/log.log"), LevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev == zap.InfoLevel }), }, { Encoder: zapcore.NewJSONEncoder(humanEncoderConfig()), WriteSyncer: getWriteSyncer("./warn/log.log"), LevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev == zap.WarnLevel }), }, { Encoder: zapcore.NewJSONEncoder(humanEncoderConfig()), WriteSyncer: getWriteSyncer("./error/log.log"), LevelEncoder: zap.LevelEnablerFunc(func(lev zapcore.Level) bool { return lev >= zap.ErrorLevel }), }, }, }) defer logger.Sync() logger.SetOutput(buf) logger.Debug("this is a debug log") // test log level assert.False(t, strings.Contains(buf.String(), "this is a debug log")) logger.Error("this is a warn log") // test log level assert.True(t, strings.Contains(buf.String(), "this is a warn log")) // test console encoder result assert.True(t, strings.Contains(buf.String(), "\tERROR\t")) logger.SetLevel(log.LevelDebug) logger.Debug("this is a debug log") assert.True(t, strings.Contains(buf.String(), "this is a debug log")) } func TestCoreConfigs(t *testing.T) { buf := new(bytes.Buffer) logger := NewLogger(LoggerConfig{ CoreConfigs: []CoreConfig{ { Encoder: zapcore.NewConsoleEncoder(humanEncoderConfig()), LevelEncoder: zap.NewAtomicLevelAt(zap.WarnLevel), WriteSyncer: zapcore.AddSync(buf), }, }, }) defer logger.Sync() // output to buffer logger.SetOutput(buf) logger.Infof("this is a info log %s", "msg") assert.False(t, strings.Contains(buf.String(), "this is a info log")) logger.Warnf("this is a warn log %s", "msg") assert.True(t, strings.Contains(buf.String(), "this is a warn log")) } // TestCoreOptions test zapcore config option func TestZapOptions(t *testing.T) { buf := new(bytes.Buffer) logger := NewLogger( LoggerConfig{ ZapOptions: []zap.Option{ zap.AddCaller(), }, }, ) defer logger.Sync() logger.SetOutput(buf) logger.Debug("this is a debug log") assert.False(t, strings.Contains(buf.String(), "this is a debug log")) logger.Error("this is a warn log") // test caller in log result assert.True(t, strings.Contains(buf.String(), "caller")) } func TestWithContextCaller(t *testing.T) { buf := new(bytes.Buffer) logger := NewLogger(LoggerConfig{ ZapOptions: []zap.Option{ zap.AddCaller(), zap.AddCallerSkip(3), }, }) logger.SetOutput(buf) logger.WithContext(context.Background()).Info("Hello, World!") var logStructMap map[string]interface{} err := json.Unmarshal(buf.Bytes(), &logStructMap) assert.Nil(t, err) value := logStructMap["caller"] caller, ok := value.(string) assert.True(t, ok) assert.Regexp(t, regexp.MustCompile(`zap/logger_test.go:\d+`), caller) } // TestWithExtraKeys test WithExtraKeys option func TestWithExtraKeys(t *testing.T) { buf := new(bytes.Buffer) logger := NewLogger(LoggerConfig{ ExtraKeys: []string{"requestId"}, }) logger.SetOutput(buf) ctx := context.WithValue(context.Background(), "requestId", "123") logger.WithContext(ctx).Infof("%s logger", "extra") var logStructMap map[string]interface{} err := json.Unmarshal(buf.Bytes(), &logStructMap) assert.Nil(t, err) value, ok := logStructMap["requestId"] assert.True(t, ok) assert.Equal(t, value, "123") } func BenchmarkNormal(b *testing.B) { buf := new(bytes.Buffer) log := NewLogger() log.SetOutput(buf) ctx := context.Background() for i := 0; i < b.N; i++ { log.WithContext(ctx).Info("normal log") } } func BenchmarkWithExtraKeys(b *testing.B) { buf := new(bytes.Buffer) logger := NewLogger(LoggerConfig{ ExtraKeys: []string{"requestId"}, }) logger.SetOutput(buf) ctx := context.WithValue(context.Background(), "requestId", "123") for i := 0; i < b.N; i++ { logger.WithContext(ctx).Info("normal logger") } } func TestCustomField(t *testing.T) { buf := new(bytes.Buffer) logger := NewLogger() log.SetLogger[*zap.Logger](logger) log.SetOutput(buf) log.Infow("", "test", "custom") var logStructMap map[string]interface{} err := json.Unmarshal(buf.Bytes(), &logStructMap) assert.Nil(t, err) value, ok := logStructMap["test"] assert.True(t, ok) assert.Equal(t, value, "custom") } ================================================ FILE: v3/zap/zap.go ================================================ package zap import ( "os" "sync" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" utilsstrings "github.com/gofiber/utils/v2/strings" "go.uber.org/zap" ) // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) // Set PID once pid := utils.FormatInt(int64(os.Getpid())) // Set variables var ( once sync.Once errHandler fiber.ErrorHandler ) var errPadding = 15 var latencyEnabled = contains("latency", cfg.Fields) // put ignore uri into a map for faster match skipURIs := make(map[string]struct{}) for _, uri := range cfg.SkipURIs { skipURIs[uri] = struct{}{} } // Return new handler return func(c fiber.Ctx) (err error) { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } // skip uri if _, ok := skipURIs[c.Path()]; ok { return c.Next() } // Set error handler once once.Do(func() { // get longested possible path stack := c.App().Stack() for m := range stack { for r := range stack[m] { if len(stack[m][r].Path) > errPadding { errPadding = len(stack[m][r].Path) } } } // override error handler errHandler = c.App().Config().ErrorHandler }) var start, stop time.Time if latencyEnabled { start = time.Now() } // Handle request, store err for logging chainErr := c.Next() // Manually call error handler if chainErr != nil { if err := errHandler(c, chainErr); err != nil { _ = c.SendStatus(fiber.StatusInternalServerError) } } // Set latency stop time if latencyEnabled { stop = time.Now() } // Check if the logger has the appropriate level var ( s = c.Response().StatusCode() index int ) switch { case s >= 500: // error index is zero case s >= 400: index = 1 default: index = 2 } levelIndex := index if levelIndex >= len(cfg.Levels) { levelIndex = len(cfg.Levels) - 1 } messageIndex := index if messageIndex >= len(cfg.Messages) { messageIndex = len(cfg.Messages) - 1 } ce := cfg.Logger.Check(cfg.Levels[levelIndex], cfg.Messages[messageIndex]) if ce == nil { return nil } // Add fields fields := make([]zap.Field, 0, len(cfg.Fields)+1) fields = append(fields, zap.Error(err)) if cfg.FieldsFunc != nil { fields = append(fields, cfg.FieldsFunc(c)...) } for _, field := range cfg.Fields { switch field { case "referer": fields = append(fields, zap.String("referer", c.Get(fiber.HeaderReferer))) case "protocol": fields = append(fields, zap.String("protocol", c.Protocol())) case "pid": fields = append(fields, zap.String("pid", pid)) case "port": fields = append(fields, zap.String("port", c.Port())) case "ip": fields = append(fields, zap.String("ip", c.IP())) case "ips": fields = append(fields, zap.String("ips", c.Get(fiber.HeaderXForwardedFor))) case "host": fields = append(fields, zap.String("host", c.Hostname())) case "path": fields = append(fields, zap.String("path", c.Path())) case "url": fields = append(fields, zap.String("url", c.OriginalURL())) case "ua": fields = append(fields, zap.String("ua", c.Get(fiber.HeaderUserAgent))) case "latency": fields = append(fields, zap.String("latency", stop.Sub(start).String())) case "status": fields = append(fields, zap.Int("status", c.Response().StatusCode())) case "resBody": if cfg.SkipResBody == nil || !cfg.SkipResBody(c) { if cfg.GetResBody == nil { fields = append(fields, zap.ByteString("resBody", c.Response().Body())) } else { fields = append(fields, zap.ByteString("resBody", cfg.GetResBody(c))) } } case "queryParams": fields = append(fields, zap.String("queryParams", c.Request().URI().QueryArgs().String())) case "body": if cfg.SkipBody == nil || !cfg.SkipBody(c) { fields = append(fields, zap.ByteString("body", c.Body())) } case "bytesReceived": fields = append(fields, zap.Int("bytesReceived", len(c.Request().Body()))) case "bytesSent": fields = append(fields, zap.Int("bytesSent", len(c.Response().Body()))) case "route": fields = append(fields, zap.String("route", c.Route().Path)) case "method": fields = append(fields, zap.String("method", c.Method())) case "requestId": fields = append(fields, zap.String("requestId", c.GetRespHeader(fiber.HeaderXRequestID))) case "error": if chainErr != nil { fields = append(fields, zap.String("error", chainErr.Error())) } case "reqHeaders": for header, values := range c.GetReqHeaders() { if len(values) == 0 { continue } sanitized := sanitizeHeaderValues(header, values) if len(sanitized) == 1 { fields = append(fields, zap.String(header, sanitized[0])) continue } fields = append(fields, zap.Strings(header, sanitized)) } } } ce.Write(fields...) return nil } } func contains(needle string, slice []string) bool { for _, e := range slice { if e == needle { return true } } return false } var sensitiveRequestHeaders = map[string]struct{}{ "authorization": {}, "proxy-authorization": {}, "cookie": {}, "x-api-key": {}, "x-auth-token": {}, } func sanitizeHeaderValues(header string, values []string) []string { if len(values) == 0 { return values } if _, ok := sensitiveRequestHeaders[utilsstrings.ToLower(header)]; !ok { return values } sanitized := make([]string, len(values)) for i := range sanitized { sanitized[i] = "[REDACTED]" } return sanitized } ================================================ FILE: v3/zap/zap_test.go ================================================ package zap import ( "bytes" "errors" "fmt" "net/http" "net/http/httptest" "os" "strconv" "sync" "testing" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/valyala/fasthttp" "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) func setupLogsCapture() (*zap.Logger, *observer.ObservedLogs) { core, logs := observer.New(zap.InfoLevel) return zap.New(core), logs } func Test_GetResBody(t *testing.T) { var readableResBody = "this is readable response body" var app = fiber.New() var logger, logs = setupLogsCapture() app.Use(New(Config{ Logger: logger, GetResBody: func(c fiber.Ctx) []byte { return []byte(readableResBody) }, Fields: []string{"resBody"}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("------this is unreadable resp------") }) _, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, readableResBody, logs.All()[0].ContextMap()["resBody"]) } // go test -run Test_SkipBody func Test_SkipBody(t *testing.T) { logger, logs := setupLogsCapture() app := fiber.New() app.Use(New(Config{ SkipBody: func(_ fiber.Ctx) bool { return true }, Logger: logger, Fields: []string{"pid", "body"}, })) body := bytes.NewReader([]byte("this is test")) resp, err := app.Test(httptest.NewRequest("GET", "/", body)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) _, ok := logs.All()[0].ContextMap()["body"] assert.Equal(t, false, ok) } // go test -run Test_SkipResBody func Test_SkipResBody(t *testing.T) { logger, logs := setupLogsCapture() app := fiber.New() app.Use(New(Config{ SkipResBody: func(_ fiber.Ctx) bool { return true }, Logger: logger, Fields: []string{"pid", "resBody"}, })) body := bytes.NewReader([]byte("this is test")) resp, err := app.Test(httptest.NewRequest("GET", "/", body)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) _, ok := logs.All()[0].ContextMap()["resBody"] assert.Equal(t, false, ok) } // go test -run Test_Logger func Test_Logger(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"pid", "latency", "error"}, })) app.Get("/", func(c fiber.Ctx) error { return errors.New("some random error") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) assert.Equal(t, "some random error", logs.All()[0].Context[3].String) } // go test -run Test_Logger_Next func Test_Logger_Next(t *testing.T) { app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { return true }, })) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) } // go test -run Test_Logger_All func Test_Logger_All(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"protocol", "pid", "body", "ip", "host", "url", "route", "method", "resBody", "queryParams", "bytesReceived", "bytesSent"}, })) resp, err := app.Test(httptest.NewRequest("GET", "/?foo=bar", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) expected := map[string]interface{}{ "body": "", "ip": "0.0.0.0", "host": "example.com", "url": "/?foo=bar", "method": "GET", "route": "/", "protocol": "HTTP/1.1", "pid": strconv.Itoa(os.Getpid()), "queryParams": "foo=bar", "resBody": "Not Found", "bytesReceived": int64(0), "bytesSent": int64(9), } assert.Equal(t, expected, logs.All()[0].ContextMap()) } // go test -run Test_Query_Params func Test_Query_Params(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"queryParams"}, })) resp, err := app.Test(httptest.NewRequest("GET", "/?foo=bar&baz=moz", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) expected := "foo=bar&baz=moz" assert.Equal(t, expected, logs.All()[0].Context[1].String) } // go test -run Test_Response_Body func Test_Response_Body(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"resBody"}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Sample response body") }) app.Post("/test", func(c fiber.Ctx) error { return c.Send([]byte("Post in test")) }) _, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) expectedGetResponse := "Sample response body" assert.Equal(t, expectedGetResponse, logs.All()[0].ContextMap()["resBody"]) _, err = app.Test(httptest.NewRequest("POST", "/test", nil)) assert.Equal(t, nil, err) expectedPostResponse := "Post in test" t.Log(logs.All()) assert.Equal(t, expectedPostResponse, logs.All()[1].ContextMap()["resBody"]) } // go test -run Test_Logger_AppendUint func Test_Logger_AppendUint(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"bytesReceived", "bytesSent", "status"}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) output := logs.All()[0].ContextMap() assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) assert.Equal(t, "0 5 200", fmt.Sprintf("%d %d %d", output["bytesReceived"], output["bytesSent"], output["status"])) } // go test -run Test_Logger_Data_Race -race func Test_Logger_Data_Race(t *testing.T) { app := fiber.New() logger := zap.NewExample() app.Use(New(Config{ Logger: logger, Fields: []string{"bytesReceived", "bytesSent", "status"}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) var ( resp1, resp2 *http.Response err1, err2 error ) wg := &sync.WaitGroup{} wg.Add(1) go func() { resp1, err1 = app.Test(httptest.NewRequest("GET", "/", nil)) wg.Done() }() resp2, err2 = app.Test(httptest.NewRequest("GET", "/", nil)) wg.Wait() assert.Equal(t, nil, err1) assert.Equal(t, fiber.StatusOK, resp1.StatusCode) assert.Equal(t, nil, err2) assert.Equal(t, fiber.StatusOK, resp2.StatusCode) } // go test -v -run=^$ -bench=Benchmark_Logger -benchmem -count=4 func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New()) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") }) h := app.Handler() fctx := &fasthttp.RequestCtx{} fctx.Request.Header.SetMethod("GET") fctx.Request.SetRequestURI("/") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { h(fctx) } assert.Equal(b, 200, fctx.Response.Header.StatusCode()) } // go test -run Test_Request_Id func Test_Request_Id(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"requestId"}, })) app.Get("/", func(c fiber.Ctx) error { c.Response().Header.Add(fiber.HeaderXRequestID, "bf985e8e-6a32-42ec-8e50-05a21db8f0e4") return c.SendString("hello") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) assert.Equal(t, "bf985e8e-6a32-42ec-8e50-05a21db8f0e4", logs.All()[0].Context[1].String) } // go test -run Test_Skip_URIs func Test_Skip_URIs(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, SkipURIs: []string{"/ignore_logging"}, })) app.Get("/ignore_logging", func(c fiber.Ctx) error { return errors.New("no log") }) resp, err := app.Test(httptest.NewRequest("GET", "/ignore_logging", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) assert.Equal(t, 0, len(logs.All())) } // go test -run Test_Req_Headers func Test_Req_Headers(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"reqHeaders"}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) expected := map[string]interface{}{ "Host": "example.com", "Baz": "foo", "Foo": "bar", } req := httptest.NewRequest("GET", "/", nil) req.Header.Add("foo", "bar") req.Header.Add("baz", "foo") resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) assert.Equal(t, expected, logs.All()[0].ContextMap()) } // go test -run Test_LoggerLevelsAndMessages func Test_LoggerLevelsAndMessages(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() levels := []zapcore.Level{zapcore.ErrorLevel, zapcore.WarnLevel, zapcore.InfoLevel} messages := []string{"server error", "client error", "success"} app.Use(New(Config{ Logger: logger, Messages: messages, Levels: levels, })) app.Get("/200", func(c fiber.Ctx) error { c.Status(fiber.StatusOK) return nil }) app.Get("/400", func(c fiber.Ctx) error { c.Status(fiber.StatusBadRequest) return nil }) app.Get("/500", func(c fiber.Ctx) error { c.Status(fiber.StatusInternalServerError) return nil }) resp, err := app.Test(httptest.NewRequest("GET", "/500", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) assert.Equal(t, levels[0], logs.All()[0].Level) assert.Equal(t, messages[0], logs.All()[0].Message) resp, err = app.Test(httptest.NewRequest("GET", "/400", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) assert.Equal(t, levels[1], logs.All()[1].Level) assert.Equal(t, messages[1], logs.All()[1].Message) resp, err = app.Test(httptest.NewRequest("GET", "/200", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) assert.Equal(t, levels[2], logs.All()[2].Level) assert.Equal(t, messages[2], logs.All()[2].Message) } // go test -run Test_LoggerLevelsAndMessagesSingle func Test_LoggerLevelsAndMessagesSingle(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() levels := []zapcore.Level{zapcore.ErrorLevel} messages := []string{"server error"} app.Use(New(Config{ Logger: logger, Messages: messages, Levels: levels, })) app.Get("/200", func(c fiber.Ctx) error { c.Status(fiber.StatusOK) return nil }) app.Get("/400", func(c fiber.Ctx) error { c.Status(fiber.StatusBadRequest) return nil }) app.Get("/500", func(c fiber.Ctx) error { c.Status(fiber.StatusInternalServerError) return nil }) resp, err := app.Test(httptest.NewRequest("GET", "/500", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) assert.Equal(t, levels[0], logs.All()[0].Level) assert.Equal(t, messages[0], logs.All()[0].Message) resp, err = app.Test(httptest.NewRequest("GET", "/400", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusBadRequest, resp.StatusCode) assert.Equal(t, levels[0], logs.All()[1].Level) assert.Equal(t, messages[0], logs.All()[1].Message) resp, err = app.Test(httptest.NewRequest("GET", "/200", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) assert.Equal(t, levels[0], logs.All()[2].Level) assert.Equal(t, messages[0], logs.All()[2].Message) } // go test -run Test_Fields_Func func Test_Fields_Func(t *testing.T) { app := fiber.New() logger, logs := setupLogsCapture() app.Use(New(Config{ Logger: logger, Fields: []string{"protocol", "pid", "body", "ip", "host", "url", "route", "method", "resBody", "queryParams", "bytesReceived", "bytesSent"}, FieldsFunc: func(c fiber.Ctx) []zap.Field { return []zap.Field{zap.String("test.custom.field", "test")} }, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "body": "", "ip": "0.0.0.0", "host": "example.com", "url": "/", "method": "GET", "route": "/", "protocol": "HTTP/1.1", "pid": strconv.Itoa(os.Getpid()), "queryParams": "", "resBody": "hello", "bytesReceived": int64(0), "bytesSent": int64(5), "test.custom.field": "test", } assert.Equal(t, expected, logs.All()[0].ContextMap()) } ================================================ FILE: v3/zerolog/README.md ================================================ --- id: zerolog --- # Zerolog ![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=*zerolog*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) ![Test](https://github.com/gofiber/contrib/workflows/Test%20zerolog/badge.svg) [Zerolog](https://zerolog.io/) logging support for Fiber. **Compatible with Fiber v3.** ## 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. ## Install ```sh go get -u github.com/gofiber/fiber/v3 go get -u github.com/gofiber/contrib/v3/zerolog go get -u github.com/rs/zerolog/log ``` ## Signature ```go zerolog.New(config ...zerolog.Config) fiber.Handler ``` ## Config | Property | Type | Description | Default | |:----------------|:------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------| | Next | `func(fiber.Ctx) bool` | Define a function to skip this middleware when it returns true. | `nil` | | Logger | `*zerolog.Logger` | Add a custom zerolog logger. | `zerolog.New(os.Stderr).With().Timestamp().Logger()` | | GetLogger | `func(fiber.Ctx) zerolog.Logger` | Get a custom zerolog logger. If set, the returned logger replaces `Logger`. | `nil` | | Fields | `[]string` | Add the fields you want to log. | `[]string{"latency", "status", "method", "url", "error"}` | | SkipField | `func(string, fiber.Ctx) bool` | Skip logging a field when it returns true. | `nil` | | SkipHeader | `func(string, fiber.Ctx) bool` | Skip logging a header when it returns true. | `nil` | | WrapHeaders | `bool` | Wrap headers into a dictionary.
If false: `{"method":"POST", "header-key":"header value"}`
If true: `{"method":"POST", "reqHeaders":{"header-key":"header value"}}` | `false` | | FieldsSnakeCase | `bool` | Use snake case for `FieldResBody`, `FieldQueryParams`, `FieldBytesReceived`, `FieldBytesSent`, `FieldRequestID`, `FieldReqHeaders`, `FieldResHeaders`.
If false: `{"method":"POST", "resBody":"v", "queryParams":"v"}`
If true: `{"method":"POST", "res_body":"v", "query_params":"v"}` | `false` | | Messages | `[]string` | Custom response messages. | `[]string{"Server error", "Client error", "Success"}` | | Levels | `[]zerolog.Level` | Custom response levels. | `[]zerolog.Level{zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel}` | | GetResBody | `func(c fiber.Ctx) []byte` | Define a function to get the response body when it returns non-nil.
For example, with compress middleware the body can be unreadable; `GetResBody` lets you provide a readable body. | `nil` | ## Example ```go package main import ( "os" middleware "github.com/gofiber/contrib/v3/zerolog" "github.com/gofiber/fiber/v3" "github.com/rs/zerolog" ) func main() { app := fiber.New() logger := zerolog.New(os.Stderr).With().Timestamp().Logger() app.Use(middleware.New(middleware.Config{ Logger: &logger, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") }) if err := app.Listen(":3000"); err != nil { logger.Fatal().Err(err).Msg("Fiber app error") } } ``` ================================================ FILE: v3/zerolog/config.go ================================================ package zerolog import ( "os" "time" "github.com/gofiber/fiber/v3" "github.com/rs/zerolog" ) const ( FieldReferer = "referer" FieldProtocol = "protocol" FieldPID = "pid" FieldPort = "port" FieldIP = "ip" FieldIPs = "ips" FieldHost = "host" FieldPath = "path" FieldURL = "url" FieldUserAgent = "ua" FieldLatency = "latency" FieldStatus = "status" FieldResBody = "resBody" FieldQueryParams = "queryParams" FieldBody = "body" FieldBytesReceived = "bytesReceived" FieldBytesSent = "bytesSent" FieldRoute = "route" FieldMethod = "method" FieldRequestID = "requestId" FieldError = "error" FieldReqHeaders = "reqHeaders" FieldResHeaders = "resHeaders" fieldResBody_ = "res_body" fieldQueryParams_ = "query_params" fieldBytesReceived_ = "bytes_received" fieldBytesSent_ = "bytes_sent" fieldRequestID_ = "request_id" fieldReqHeaders_ = "req_headers" fieldResHeaders_ = "res_headers" ) // Config defines the config for middleware. type Config struct { // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool // SkipField defines a function that returns true if a specific field should be skipped from logging. // // Optional. Default: nil SkipField func(field string, c fiber.Ctx) bool // GetResBody defines a function to get a custom response body. // e.g. when using compress middleware, the original response body may be unreadable. // You can use GetResBody to provide a readable body. // // Optional. Default: nil GetResBody func(c fiber.Ctx) []byte // Add a custom zerolog logger. // // Optional. Default: zerolog.New(os.Stderr).With().Timestamp().Logger() Logger *zerolog.Logger // GetLogger defines a function to get a custom zerolog logger. // e.g. when creating a new logger for each request. // // GetLogger will override Logger. // // Optional. Default: nil GetLogger func(c fiber.Ctx) zerolog.Logger // Add the fields you want to log. // // Optional. Default: {"ip", "latency", "status", "method", "url", "error"} Fields []string // Defines a function that returns true if a header should not be logged. // Only relevant if `FieldReqHeaders` and/or `FieldResHeaders` are logged. // // Optional. Default: nil SkipHeader func(header string, c fiber.Ctx) bool // Wrap headers into a dictionary. // If false: {"method":"POST", "header-key":"header value"} // If true: {"method":"POST", "reqHeaders": {"header-key":"header value"}} // // Optional. Default: false WrapHeaders bool // Use snake case for fields: FieldResBody, FieldQueryParams, FieldBytesReceived, FieldBytesSent, FieldRequestID, FieldReqHeaders, FieldResHeaders. // If false: {"method":"POST", "resBody":"v", "queryParams":"v"} // If true: {"method":"POST", "res_body":"v", "query_params":"v"} // // Optional. Default: false FieldsSnakeCase bool // Custom response messages. // Response codes >= 500 will be logged with Messages[0]. // Response codes >= 400 will be logged with Messages[1]. // Other response codes will be logged with Messages[2]. // You can specify fewer than 3 messages, but you must specify at least 1. // Specifying more than 3 messages is useless. // // Optional. Default: {"Server error", "Client error", "Success"} Messages []string // Custom response levels. // Response codes >= 500 will be logged with Levels[0]. // Response codes >= 400 will be logged with Levels[1]. // Other response codes will be logged with Levels[2]. // You can specify fewer than 3 levels, but you must specify at least 1. // Specifying more than 3 levels is useless. // // Optional. Default: {zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel} Levels []zerolog.Level } func (c *Config) loggerCtx(fc fiber.Ctx) zerolog.Context { if c.GetLogger != nil { return c.GetLogger(fc).With() } return c.Logger.With() } func (c *Config) logger(fc fiber.Ctx, latency time.Duration, err error) zerolog.Logger { zc := c.loggerCtx(fc) for _, field := range c.Fields { if c.SkipField != nil && c.SkipField(field, fc) { continue } switch field { case FieldReferer: zc = zc.Str(field, fc.Get(fiber.HeaderReferer)) case FieldProtocol: zc = zc.Str(field, fc.Protocol()) case FieldPID: zc = zc.Int(field, os.Getpid()) case FieldPort: zc = zc.Str(field, fc.Port()) case FieldIP: zc = zc.Str(field, fc.IP()) case FieldIPs: zc = zc.Str(field, fc.Get(fiber.HeaderXForwardedFor)) case FieldHost: zc = zc.Str(field, fc.Hostname()) case FieldPath: zc = zc.Str(field, fc.Path()) case FieldURL: zc = zc.Str(field, fc.OriginalURL()) case FieldUserAgent: zc = zc.Str(field, fc.Get(fiber.HeaderUserAgent)) case FieldLatency: zc = zc.Str(field, latency.String()) case FieldStatus: zc = zc.Int(field, fc.Response().StatusCode()) case FieldBody: zc = zc.Str(field, string(fc.Body())) case FieldResBody: if c.FieldsSnakeCase { field = fieldResBody_ } resBody := fc.Response().Body() if c.GetResBody != nil { if customResBody := c.GetResBody(fc); customResBody != nil { resBody = customResBody } } zc = zc.Str(field, string(resBody)) case FieldQueryParams: if c.FieldsSnakeCase { field = fieldQueryParams_ } zc = zc.Stringer(field, fc.Request().URI().QueryArgs()) case FieldBytesReceived: if c.FieldsSnakeCase { field = fieldBytesReceived_ } zc = zc.Int(field, len(fc.Request().Body())) case FieldBytesSent: if c.FieldsSnakeCase { field = fieldBytesSent_ } zc = zc.Int(field, len(fc.Response().Body())) case FieldRoute: zc = zc.Str(field, fc.Route().Path) case FieldMethod: zc = zc.Str(field, fc.Method()) case FieldRequestID: if c.FieldsSnakeCase { field = fieldRequestID_ } zc = zc.Str(field, fc.GetRespHeader(fiber.HeaderXRequestID)) case FieldError: if err != nil { zc = zc.Err(err) } case FieldReqHeaders: if c.FieldsSnakeCase { field = fieldReqHeaders_ } if c.WrapHeaders { dict := zerolog.Dict() for header, values := range fc.GetReqHeaders() { if len(values) == 0 { continue } if c.SkipHeader != nil && c.SkipHeader(header, fc) { continue } if len(values) == 1 { dict.Str(header, values[0]) continue } dict.Strs(header, values) } zc = zc.Dict(field, dict) } else { for header, values := range fc.GetReqHeaders() { if len(values) == 0 { continue } if c.SkipHeader != nil && c.SkipHeader(header, fc) { continue } if len(values) == 1 { zc = zc.Str(header, values[0]) continue } zc = zc.Strs(header, values) } } case FieldResHeaders: if c.FieldsSnakeCase { field = fieldResHeaders_ } if c.WrapHeaders { dict := zerolog.Dict() for header, values := range fc.GetRespHeaders() { if len(values) == 0 { continue } if c.SkipHeader != nil && c.SkipHeader(header, fc) { continue } if len(values) == 1 { dict.Str(header, values[0]) continue } dict.Strs(header, values) } zc = zc.Dict(field, dict) } else { for header, values := range fc.GetRespHeaders() { if len(values) == 0 { continue } if c.SkipHeader != nil && c.SkipHeader(header, fc) { continue } if len(values) == 1 { zc = zc.Str(header, values[0]) continue } zc = zc.Strs(header, values) } } } } return zc.Logger() } var logger = zerolog.New(os.Stderr).With().Timestamp().Logger() // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, Logger: &logger, Fields: []string{FieldIP, FieldLatency, FieldStatus, FieldMethod, FieldURL, FieldError}, Messages: []string{"Server error", "Client error", "Success"}, Levels: []zerolog.Level{zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel}, } // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided if len(config) < 1 { return ConfigDefault } // Override default config cfg := config[0] // Set default values if cfg.Next == nil { cfg.Next = ConfigDefault.Next } if cfg.Logger == nil { cfg.Logger = ConfigDefault.Logger } if cfg.Fields == nil { cfg.Fields = ConfigDefault.Fields } if cfg.Messages == nil { cfg.Messages = ConfigDefault.Messages } if cfg.Levels == nil { cfg.Levels = ConfigDefault.Levels } return cfg } ================================================ FILE: v3/zerolog/go.mod ================================================ module github.com/gofiber/contrib/v3/zerolog go 1.25.0 require ( github.com/gofiber/fiber/v3 v3.1.0 github.com/rs/zerolog v1.35.0 github.com/stretchr/testify v1.11.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gofiber/schema v1.7.1 // indirect github.com/gofiber/utils/v2 v2.0.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.70.0 // indirect golang.org/x/crypto v0.50.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: v3/zerolog/go.sum ================================================ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY= github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU= github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI= github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk= github.com/gofiber/utils/v2 v2.0.3 h1:qJyfS/t7s7Z4+/zlU1i1pafYNP2+xLupVPgkW8ce1uI= github.com/gofiber/utils/v2 v2.0.3/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU= github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: v3/zerolog/zerolog.go ================================================ package zerolog import ( "time" "github.com/gofiber/fiber/v3" "github.com/rs/zerolog" ) // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) // Return new handler return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true if cfg.Next != nil && cfg.Next(c) { return c.Next() } start := time.Now() // Handle request, store err for logging chainErr := c.Next() if chainErr != nil { // Manually call error handler if err := c.App().ErrorHandler(c, chainErr); err != nil { _ = c.SendStatus(fiber.StatusInternalServerError) } } latency := time.Since(start) status := c.Response().StatusCode() index := 0 switch { case status >= 500: // error index is zero case status >= 400: index = 1 default: index = 2 } levelIndex := index if levelIndex >= len(cfg.Levels) { levelIndex = len(cfg.Levels) - 1 } level := cfg.Levels[levelIndex] // no log if level == zerolog.NoLevel || level == zerolog.Disabled { return nil } messageIndex := index if messageIndex >= len(cfg.Messages) { messageIndex = len(cfg.Messages) - 1 } message := cfg.Messages[messageIndex] logger := cfg.logger(c, latency, chainErr) ctx := c switch level { case zerolog.DebugLevel: logger.Debug().Ctx(ctx).Msg(message) case zerolog.InfoLevel: logger.Info().Ctx(ctx).Msg(message) case zerolog.WarnLevel: logger.Warn().Ctx(ctx).Msg(message) case zerolog.ErrorLevel: logger.Error().Ctx(ctx).Msg(message) case zerolog.FatalLevel: logger.Fatal().Ctx(ctx).Msg(message) case zerolog.PanicLevel: logger.Panic().Ctx(ctx).Msg(message) case zerolog.TraceLevel: logger.Trace().Ctx(ctx).Msg(message) } return nil } } ================================================ FILE: v3/zerolog/zerolog_test.go ================================================ package zerolog import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/requestid" "github.com/rs/zerolog" ) func Test_GetResBody(t *testing.T) { t.Parallel() readableResBody := "this is readable response body" var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, GetResBody: func(c fiber.Ctx) []byte { return []byte(readableResBody) }, Fields: []string{FieldResBody}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("------this is unreadable resp------") }) _, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, readableResBody, logs[FieldResBody]) } func Test_SkipBody(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ SkipField: func(field string, _ fiber.Ctx) bool { return field == FieldBody }, Logger: &logger, Fields: []string{FieldPID, FieldBody}, })) body := bytes.NewReader([]byte("this is test")) resp, err := app.Test(httptest.NewRequest("GET", "/", body)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) _, ok := logs[FieldBody] assert.Equal(t, false, ok) } func Test_SkipResBody(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ SkipField: func(field string, _ fiber.Ctx) bool { return field == FieldResBody }, Logger: &logger, Fields: []string{FieldResBody}, })) body := bytes.NewReader([]byte("this is test")) resp, err := app.Test(httptest.NewRequest("GET", "/", body)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) _, ok := logs[FieldResBody] assert.Equal(t, false, ok) } func Test_Logger(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, })) app.Get("/", func(c fiber.Ctx) error { return errors.New("some random error") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, "some random error", logs[FieldError]) assert.Equal(t, float64(500), logs[FieldStatus]) } func Test_Latency(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, })) app.Get("/", func(c fiber.Ctx) error { time.Sleep(100 * time.Millisecond) return c.SendStatus(fiber.StatusOK) }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) latencyStr, ok := logs[FieldLatency].(string) assert.Equal(t, true, ok) assert.Equal(t, true, strings.Contains(latencyStr, "ms")) assert.Equal(t, float64(200), logs[FieldStatus]) } func Test_Logger_Next(t *testing.T) { t.Parallel() app := fiber.New() app.Use(New(Config{ Next: func(_ fiber.Ctx) bool { return true }, })) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) } func Test_Logger_All(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{ FieldProtocol, FieldPID, FieldBody, FieldIP, FieldHost, FieldURL, FieldLatency, FieldRoute, FieldMethod, FieldResBody, FieldQueryParams, FieldBytesReceived, FieldBytesSent, }, })) app.Get("/", func(c fiber.Ctx) error { time.Sleep(100 * time.Millisecond) return c.SendStatus(fiber.StatusNotFound) }) resp, err := app.Test(httptest.NewRequest("GET", "/?foo=bar", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusNotFound, resp.StatusCode) expected := map[string]interface{}{ "body": "", "ip": "0.0.0.0", "host": "example.com", "url": "/?foo=bar", "level": "warn", "message": "Client error", "method": "GET", "route": "/", "protocol": "HTTP/1.1", "pid": float64(os.Getpid()), "queryParams": "foo=bar", "resBody": "Not Found", "bytesReceived": float64(0), "bytesSent": float64(9), } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) for key, value := range expected { assert.Equal(t, value, logs[key]) } latencyStr, ok := logs[FieldLatency].(string) assert.Equal(t, true, ok) assert.Equal(t, true, strings.Contains(latencyStr, "ms")) } func Test_Response_Body(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldResBody}, })) expectedGetResponse := "Sample response body" app.Get("/", func(c fiber.Ctx) error { return c.SendString(expectedGetResponse) }) _, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expectedGetResponse, logs[FieldResBody]) } func Test_Request_Id(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldRequestID}, })) requestID := "bf985e8e-6a32-42ec-8e50-05a21db8f0e4" app.Get("/", func(c fiber.Ctx) error { c.Response().Header.Set(fiber.HeaderXRequestID, requestID) return c.SendString("hello") }) resp, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, requestID, logs[FieldRequestID]) } func Test_Skip_URIs(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Next: func(c fiber.Ctx) bool { return c.Path() == "/ignore_logging" }, })) app.Get("/ignore_logging", func(c fiber.Ctx) error { return errors.New("no log") }) resp, err := app.Test(httptest.NewRequest("GET", "/ignore_logging", nil)) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusInternalServerError, resp.StatusCode) assert.Equal(t, 0, buf.Len()) } func Test_Req_Headers(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldReqHeaders}, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) req.Header.Add("foo", "bar") req.Header.Add("baz", "foo") resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "Host": "example.com", "Baz": "foo", "Foo": "bar", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_Req_Headers_WrapHeaders(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldReqHeaders}, WrapHeaders: true, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) req.Header.Add("foo", "bar") req.Header.Add("baz", "foo") resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "reqHeaders": map[string]interface{}{ "Host": "example.com", "Baz": "foo", "Foo": "bar", }, "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_Res_Headers(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldResHeaders}, })) app.Get("/", func(c fiber.Ctx) error { c.Set("foo", "bar") c.Set("baz", "foo") return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "Content-Type": "text/plain; charset=utf-8", "Baz": "foo", "Foo": "bar", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_Res_Headers_WrapHeaders(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldResHeaders}, WrapHeaders: true, })) app.Get("/", func(c fiber.Ctx) error { c.Set("foo", "bar") c.Set("baz", "foo") return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "resHeaders": map[string]interface{}{ "Content-Type": "text/plain; charset=utf-8", "Baz": "foo", "Foo": "bar", }, "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_FieldsSnakeCase(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(requestid.New()) app.Use(New(Config{ Logger: &logger, Fields: []string{ FieldResBody, FieldQueryParams, FieldBytesReceived, FieldBytesSent, FieldRequestID, FieldResHeaders, FieldReqHeaders, }, FieldsSnakeCase: true, WrapHeaders: true, })) app.Get("/", func(c fiber.Ctx) error { c.Set("Foo", "bar") return c.SendString("hello") }) req := httptest.NewRequest("GET", "/?param=value", nil) req.Header.Add("X-Request-ID", "uuid") req.Header.Add("Baz", "foo") resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "bytes_received": float64(0), "bytes_sent": float64(5), "query_params": "param=value", "req_headers": map[string]interface{}{ "Host": "example.com", "Baz": "foo", "X-Request-Id": "uuid", }, "res_headers": map[string]interface{}{ "Content-Type": "text/plain; charset=utf-8", "Foo": "bar", "X-Request-Id": "uuid", }, "request_id": "uuid", "res_body": "hello", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_LoggerLevelsAndMessages(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) levels := []zerolog.Level{zerolog.ErrorLevel, zerolog.WarnLevel, zerolog.InfoLevel} messages := []string{"server error", "client error", "success"} app := fiber.New() app.Use(New(Config{ Logger: &logger, Messages: messages, Levels: levels, })) app.Get("/200", func(c fiber.Ctx) error { c.Status(fiber.StatusOK) return nil }) app.Get("/400", func(c fiber.Ctx) error { c.Status(fiber.StatusBadRequest) return nil }) app.Get("/500", func(c fiber.Ctx) error { c.Status(fiber.StatusInternalServerError) return nil }) tests := []struct { Req *http.Request Status int Level string Message string }{ { Req: httptest.NewRequest("GET", "/500", nil), Status: fiber.StatusInternalServerError, Level: levels[0].String(), Message: messages[0], }, { Req: httptest.NewRequest("GET", "/400", nil), Status: fiber.StatusBadRequest, Level: levels[1].String(), Message: messages[1], }, { Req: httptest.NewRequest("GET", "/200", nil), Status: fiber.StatusOK, Level: levels[2].String(), Message: messages[2], }, } for _, test := range tests { name := fmt.Sprintf("%s %s", test.Req.Method, test.Req.URL) t.Run(name, func(t *testing.T) { buf.Reset() resp, err := app.Test(test.Req) assert.Equal(t, nil, err) assert.Equal(t, test.Status, resp.StatusCode) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, test.Level, logs["level"]) assert.Equal(t, test.Message, logs["message"]) }) } } func Test_Logger_FromContext(t *testing.T) { t.Parallel() var buf bytes.Buffer app := fiber.New() app.Use(New(Config{ GetLogger: func(c fiber.Ctx) zerolog.Logger { return zerolog.New(&buf). With(). Str("foo", "bar"). Logger() }, })) _, err := app.Test(httptest.NewRequest("GET", "/", nil)) assert.Equal(t, nil, err) var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, "bar", logs["foo"]) } func Test_Logger_WhitelistHeaders(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldReqHeaders}, SkipHeader: func(header string, _ fiber.Ctx) bool { switch header { case "Foo", "Host", "Bar": return false default: return true } }, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) req.Header.Add("foo", "bar") req.Header.Add("baz", "foo") resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "Host": "example.com", "Foo": "bar", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) app.Get("/res-headers", func(c fiber.Ctx) error { c.Set("test", "skip") c.Set("bar", "bar") return c.SendString("hello") }) req = httptest.NewRequest("GET", "/res-headers", nil) req.Header.Add("foo", "bar") req.Header.Add("baz", "foo") resp, err = app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected = map[string]interface{}{ "Bar": "bar", "level": "info", "message": "Success", } } func Test_WhitelistHeaders_Resp_Headers(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldResHeaders}, SkipHeader: func(header string, _ fiber.Ctx) bool { return header != "Bar" }, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "skip") c.Set("bar", "bar") return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "Bar": "bar", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_Logger_BlacklistHeaders(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldReqHeaders}, SkipHeader: func(header string, _ fiber.Ctx) bool { return header == "Foo" }, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) req.Header.Add("foo", "bar") req.Header.Add("baz", "foo") resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "Host": "example.com", "Baz": "foo", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) } func Test_BlacklistHeaders_Resp_Headers(t *testing.T) { t.Parallel() var buf bytes.Buffer logger := zerolog.New(&buf) app := fiber.New() app.Use(New(Config{ Logger: &logger, Fields: []string{FieldResHeaders}, SkipHeader: func(header string, _ fiber.Ctx) bool { return header == "Test" }, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "skip") c.Set("bar", "bar") return c.SendString("hello") }) req := httptest.NewRequest("GET", "/", nil) resp, err := app.Test(req) assert.Equal(t, nil, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) expected := map[string]interface{}{ "Bar": "bar", "Content-Type": "text/plain; charset=utf-8", "level": "info", "message": "Success", } var logs map[string]any _ = json.Unmarshal(buf.Bytes(), &logs) assert.Equal(t, expected, logs) }