Skip to main content
ImmutableLog logo
Back
GoFiber

Fiber — ImmutableLog

Middleware for Fiber v2. Uses c.Next() to capture status and errors, c.Locals() for custom events, and goroutine fire-and-forget for zero overhead. Includes correct handling of fasthttp buffer recycling.

Middleware code

Fiber uses fasthttp internally (not net/http). The middleware copies the body before c.Next() because Fiber recycles buffers after each request.

go
package immutablelog

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
)

const fiberEventNameKey = "imtbl.eventName"

type Config struct {
	APIKey      string
	ServiceName string
	Env         string
	APIURL      string
	SkipPaths   []string
}

// New returns a fiber.Handler middleware that sends an audit event to ImmutableLog.
// The event is sent asynchronously via a goroutine — never blocks the response.
func New(cfg Config) fiber.Handler {
	if cfg.APIURL == "" {
		cfg.APIURL = "https://api.immutablelog.com"
	}
	if cfg.APIKey == "" {
		cfg.APIKey = os.Getenv("IMTBL_API_KEY")
	}
	if cfg.ServiceName == "" {
		cfg.ServiceName = os.Getenv("IMTBL_SERVICE_NAME")
	}
	if cfg.Env == "" {
		cfg.Env = os.Getenv("IMTBL_ENV")
	}

	skipSet := make(map[string]struct{}, len(cfg.SkipPaths))
	for _, p := range cfg.SkipPaths {
		skipSet[p] = struct{}{}
	}

	return func(c *fiber.Ctx) error {
		if _, skip := skipSet[c.Path()]; skip {
			return c.Next()
		}

		startedAt := time.Now()

		// Copy body BEFORE c.Next() — Fiber recycles the buffer after the request
		bodyBytes := make([]byte, len(c.Body()))
		copy(bodyBytes, c.Body())

		// Execute all downstream handlers
		err := c.Next()

		elapsed := time.Since(startedAt)
		status := c.Response().StatusCode()

		// Custom event name set by a handler via c.Locals(fiberEventNameKey, "...")
		eventName, _ := c.Locals(fiberEventNameKey).(string)

		go emit(c, status, elapsed, bodyBytes, eventName, err, cfg)

		return err
	}
}

type eventPayload struct {
	Method          string `json:"method"`
	Path            string `json:"path"`
	Status          int    `json:"status"`
	LatencyMs       int64  `json:"latency_ms"`
	ClientIP        string `json:"client_ip"`
	UserAgent       string `json:"user_agent,omitempty"`
	RequestBodyHash string `json:"request_body_hash,omitempty"`
	ErrorMessage    string `json:"error_message,omitempty"`
}

type eventMeta struct {
	Type      string `json:"type"`
	EventName string `json:"event_name"`
	Service   string `json:"service"`
	Env       string `json:"env"`
	RequestID string `json:"request_id"`
}

type eventBody struct {
	Payload string    `json:"payload"`
	Meta    eventMeta `json:"meta"`
}

func emit(c *fiber.Ctx, status int, elapsed time.Duration,
	body []byte, customName string, handlerErr error, cfg Config) {

	requestID := c.Get("X-Request-Id")
	if requestID == "" {
		requestID = fmt.Sprintf("%d", time.Now().UnixNano())
	}

	eventName := customName
	if eventName == "" {
		eventName = strings.ToLower(c.Method()) + "." +
			strings.ReplaceAll(strings.TrimPrefix(c.Path(), "/"), "/", ".")
	}

	kind := "success"
	if status >= 400 || handlerErr != nil {
		kind = "error"
	} else if status >= 300 {
		kind = "info"
	}

	p := eventPayload{
		Method:    c.Method(),
		Path:      c.Path(),
		Status:    status,
		LatencyMs: elapsed.Milliseconds(),
		ClientIP:  c.IP(),
		UserAgent: string(c.Request().Header.UserAgent()),
	}
	if len(body) > 0 {
		p.RequestBodyHash = sha256Hex(body)
	}
	if handlerErr != nil {
		p.ErrorMessage = handlerErr.Error()
	}

	payloadJSON, _ := json.Marshal(p)

	evt := eventBody{
		Payload: string(payloadJSON),
		Meta: eventMeta{
			Type:      kind,
			EventName: eventName,
			Service:   cfg.ServiceName,
			Env:       cfg.Env,
			RequestID: requestID,
		},
	}
	evtJSON, _ := json.Marshal(evt)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		cfg.APIURL+"/v1/events", bytes.NewBuffer(evtJSON))
	if err != nil {
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+cfg.APIKey)
	req.Header.Set("Idempotency-Key", requestID)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()
}

func sha256Hex(data []byte) string {
	h := sha256.Sum256(data)
	return fmt.Sprintf("%x", h)
}

Global registration

go
package main

import (
	"github.com/gofiber/fiber/v2"
	"github.com/yourorg/yourapp/immutablelog"
)

func main() {
	app := fiber.New()

	// Register globally — applies to all routes
	app.Use(immutablelog.New(immutablelog.Config{
		APIKey:      "iml_live_xxxx",
		ServiceName: "my-api",
		Env:         "production",
		SkipPaths:   []string{"/health", "/metrics"},
	}))

	app.Get("/health", func(c *fiber.Ctx) error {
		return c.JSON(fiber.Map{"ok": true})
	})
	app.Post("/payments", paymentsHandler)

	app.Listen(":8080")
}

How it works

1. c.Next() + c.Response().StatusCode()

c.Next() executes the full handler chain. After it returns, c.Response().StatusCode() contains the final status code set by the handlers.

2. Error capture via return

In Go/Fiber, errors are returned — not thrown. The middleware captures the error returned by c.Next() and includes it in the event if not nil, along with status 500.

3. c.IP()

Fiber's c.IP() considers X-Forwarded-For automatically when ProxyHeader is configured. Set it via fiber.Config{ProxyHeader: "X-Forwarded-For"} at startup.

Custom event

Use c.Locals() to pass the event name from the handler to the middleware. Locals persists for the full request lifecycle.

go
func paymentsHandler(c *fiber.Ctx) error {
	// Set custom event name via Locals — middleware reads after c.Next()
	c.Locals("imtbl.eventName", "payment.created")

	var req PaymentRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(400).JSON(fiber.Map{"error": err.Error()})
	}

	// ... process payment ...
	return c.Status(201).JSON(fiber.Map{"ok": true, "payment_id": "pay_123"})
}

Body recycling

Fiber (fasthttp) reuses request/response buffers for high performance. You must copy the body before c.Next() to ensure the hash is computed over the correct data.

go
// Fiber (fasthttp) recycles request buffers for high performance.
// Always copy the body BEFORE calling c.Next():

bodyBytes := make([]byte, len(c.Body()))
copy(bodyBytes, c.Body())

err := c.Next()
// After this, c.Body() may be empty or contain data from another request.
// Use bodyBytes for hashing — it has a stable copy.

ErrorHandler

Fiber's global ErrorHandler processes errors returned by handlers. The audit middleware captures the error BEFORE ErrorHandler runs — status and error_message are included in the audit event.

go
// Fiber's global ErrorHandler processes errors returned by handlers.
// The audit middleware captures the error returned from c.Next()
// BEFORE ErrorHandler runs — it's included in the event as error_message.

app := fiber.New(fiber.Config{
	ErrorHandler: func(c *fiber.Ctx, err error) error {
		code := fiber.StatusInternalServerError
		if e, ok := err.(*fiber.Error); ok {
			code = e.Code
		}
		return c.Status(code).JSON(fiber.Map{"error": err.Error()})
	},
})

// Handler that returns an error:
app.Get("/fail", func(c *fiber.Ctx) error {
	return fiber.NewError(500, "something went wrong")
	// audit middleware captures: status=500, error_message="something went wrong"
})

This documentation reflects the current integration behavior. For questions or advanced integrations, contact the support team.