-
Notifications
You must be signed in to change notification settings - Fork 139
Add guides for error handling, rate limiting, testing patterns, and structured logging in Fiber v3 #517
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add guides for error handling, rate limiting, testing patterns, and structured logging in Fiber v3 #517
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
dc1cf0d
Add guides for error handling, rate limiting, testing patterns, and s…
ReneWerner87 0141af0
Enhance error handling and rate limiting guides for Fiber v3
ReneWerner87 65855c1
Add spell check workflow and configuration for improved code quality
ReneWerner87 32be962
Merge branch 'master' into more-blog-entries
ReneWerner87 98e391e
Enhance spell check configuration and add Makefile for development tasks
ReneWerner87 0921262
Merge remote-tracking branch 'origin/more-blog-entries' into more-blo…
ReneWerner87 eba9b31
Update spell-check configuration to include blog and src directories
ReneWerner87 d88cc53
Use npx to run markdownlint-cli2 for blog markdown files
ReneWerner87 464bc0e
Update links in blog entries to use relative paths
ReneWerner87 afe821f
Update blog entries to use absolute paths for internal references
ReneWerner87 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| --- | ||
| slug: fiber-v3-error-handling-production | ||
| title: Error Handling That Doesn't Embarrass You in Production | ||
| authors: [fiber-team] | ||
| tags: [fiber, v3, error-handling, production, security, go] | ||
| description: Build a custom error handler in Fiber v3 that keeps your internals hidden while giving clients useful, structured responses. | ||
| --- | ||
|
|
||
| The scariest thing in production is not that errors happen. It is that the wrong information leaks when they do. | ||
|
|
||
| In Fiber v3, the default error handler sends `err.Error()` to the client, which can expose a raw database error, an internal file path, or other library-generated details. Panics are a separate concern: Fiber does not recover them by default, so they crash the process unless you enable the Recover middleware. During development, that kind of visibility is helpful. In production, it is a security incident waiting to happen. An attacker learns your ORM, your table schema, maybe even the specific query that failed. All from a 500 response you never thought anyone would read. | ||
|
|
||
| Fiber v3's error handling is designed around one idea: handlers return errors, and a central handler decides what the client sees. That separation sounds simple, but it changes how you structure error responses across your entire application. | ||
|
|
||
| <!-- truncate --> | ||
|
|
||
| ## Why the Default Handler Is Not Enough | ||
|
|
||
| Fiber ships with a default error handler that does the right thing for prototyping: it checks if the error is a `*fiber.Error`, extracts the status code, and sends the error message as plain text. | ||
|
|
||
| ```go | ||
| var DefaultErrorHandler = func(c fiber.Ctx, err error) error { | ||
| code := fiber.StatusInternalServerError | ||
| var e *fiber.Error | ||
| if errors.As(err, &e) { | ||
| code = e.Code | ||
| } | ||
| c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) | ||
| return c.Status(code).SendString(err.Error()) | ||
| } | ||
| ``` | ||
|
|
||
| The problem is `err.Error()`. For a `fiber.Error`, the message is controlled. But for an unexpected error — a database timeout, a nil pointer, a failed file operation — `err.Error()` contains whatever the underlying library decided to put in there. That string goes straight to the client. | ||
|
|
||
| In one real-world incident, a team discovered that their Postgres connection error included the DSN with credentials. The default handler dutifully sent it as the response body. | ||
|
|
||
| ## Building a Production Error Handler | ||
|
|
||
| A production error handler needs to do three things: classify the error, log the details internally, and send a sanitized response to the client. | ||
|
|
||
| ```go | ||
| type APIError struct { | ||
| Code int `json:"code"` | ||
| Message string `json:"message"` | ||
| TraceID string `json:"trace_id,omitempty"` | ||
| } | ||
|
|
||
| app := fiber.New(fiber.Config{ | ||
| ErrorHandler: func(c fiber.Ctx, err error) error { | ||
| code := fiber.StatusInternalServerError | ||
| message := "An unexpected error occurred" | ||
|
|
||
| var e *fiber.Error | ||
| if errors.As(err, &e) { | ||
| code = e.Code | ||
| message = e.Message | ||
| } | ||
|
|
||
| traceID := requestid.FromContext(c) | ||
|
|
||
| // Log the full error internally — never send this to the client | ||
| log.Printf("[%s] %d %s %s: %v", | ||
| traceID, code, c.Method(), c.Path(), err) | ||
|
|
||
| return c.Status(code).JSON(APIError{ | ||
| Code: code, | ||
| Message: message, | ||
| TraceID: traceID, | ||
| }) | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| The key insight is the split between `err` (logged, never exposed) and `message` (sent to the client, always controlled). If the error is a `*fiber.Error`, the message was explicitly set by your code. If it is anything else, the client gets a generic message and the real error goes to your logs. | ||
|
|
||
| ## Layered Error Types | ||
|
|
||
| For a larger application, you probably want more than just `fiber.Error`. Consider a domain-specific error type that carries both public and private information: | ||
|
|
||
| ```go | ||
| type AppError struct { | ||
| StatusCode int | ||
| PublicMsg string | ||
| Internal error | ||
| } | ||
|
|
||
| func (e *AppError) Error() string { | ||
| if e.Internal != nil { | ||
| return e.Internal.Error() | ||
| } | ||
| return e.PublicMsg | ||
| } | ||
|
|
||
| func NewNotFound(msg string, internal error) *AppError { | ||
| return &AppError{ | ||
| StatusCode: fiber.StatusNotFound, | ||
| PublicMsg: msg, | ||
| Internal: internal, | ||
| } | ||
| } | ||
|
|
||
| func NewBadRequest(msg string) *AppError { | ||
| return &AppError{ | ||
| StatusCode: fiber.StatusBadRequest, | ||
| PublicMsg: msg, | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Then your handlers become expressive without leaking internals: | ||
|
|
||
| ```go | ||
| app.Get("/users/:id", func(c fiber.Ctx) error { | ||
| user, err := db.FindUser(c.Params("id")) | ||
| if err != nil { | ||
| if errors.Is(err, sql.ErrNoRows) { | ||
| return NewNotFound("User not found", err) | ||
| } | ||
| return err // Will hit the generic "unexpected error" path | ||
| } | ||
| return c.JSON(user) | ||
| }) | ||
| ``` | ||
|
|
||
| And the error handler checks for your type first: | ||
|
|
||
| ```go | ||
| ErrorHandler: func(c fiber.Ctx, err error) error { | ||
| code := fiber.StatusInternalServerError | ||
| message := "An unexpected error occurred" | ||
|
|
||
| var appErr *AppError | ||
| var fiberErr *fiber.Error | ||
|
|
||
| switch { | ||
| case errors.As(err, &appErr): | ||
| code = appErr.StatusCode | ||
| message = appErr.PublicMsg | ||
| if appErr.Internal != nil { | ||
| log.Printf("internal: %v", appErr.Internal) | ||
| } | ||
| case errors.As(err, &fiberErr): | ||
| code = fiberErr.Code | ||
| message = fiberErr.Message | ||
| default: | ||
| log.Printf("unhandled: %v", err) | ||
| } | ||
|
|
||
| return c.Status(code).JSON(APIError{ | ||
| Code: code, | ||
| Message: message, | ||
| }) | ||
| } | ||
| ``` | ||
|
|
||
| ## Don't Forget Panics | ||
|
|
||
| Fiber does not recover from panics by default. If a nil pointer dereference hits your handler, the entire process crashes. The `recover` middleware catches panics and converts them to errors that flow through your error handler: | ||
|
|
||
| ```go | ||
| import recoverer "github.com/gofiber/fiber/v3/middleware/recover" | ||
|
|
||
| app.Use(recoverer.New()) | ||
| ``` | ||
|
|
||
| Note the import alias: Go's built-in `recover` keyword conflicts with the package name, so the convention is to import it as `recoverer`. | ||
|
|
||
| With your custom error handler in place, a panic becomes a logged internal error and a clean 500 response instead of a process restart and a confused load balancer. | ||
|
|
||
| One subtlety: the recover middleware should be registered early, before other middleware. If it is registered after the logger, panics in the logger itself will not be caught. | ||
|
|
||
| ## Validation Errors as Structured Responses | ||
|
|
||
| Validation failures deserve special treatment. A 400 response that says "bad request" is useless to API consumers. They need to know which field failed and why. | ||
|
|
||
| ```go | ||
| type ValidationError struct { | ||
| Field string `json:"field"` | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| type ValidationErrors struct { | ||
| Errors []ValidationError `json:"errors"` | ||
| } | ||
|
|
||
| func (e *ValidationErrors) Error() string { | ||
| return fmt.Sprintf("%d validation errors", len(e.Errors)) | ||
| } | ||
| ``` | ||
|
|
||
| In the error handler, check for this type and return a 422: | ||
|
|
||
| ```go | ||
| var validationErr *ValidationErrors | ||
| if errors.As(err, &validationErr) { | ||
| return c.Status(fiber.StatusUnprocessableEntity).JSON(validationErr) | ||
| } | ||
| ``` | ||
|
|
||
| This pattern works well with `go-playground/validator` or any validation library that returns structured errors. | ||
|
|
||
| ## Content Negotiation | ||
|
|
||
| One detail that is easy to miss: not all clients want JSON. If your application serves both an API and HTML pages, the error handler should respect the `Accept` header: | ||
|
|
||
| ```go | ||
| if c.Accepts("application/json") != "" { | ||
| return c.Status(code).JSON(APIError{Code: code, Message: message}) | ||
| } | ||
| return c.Status(code).Render("error", fiber.Map{ | ||
| "Code": code, | ||
| "Message": message, | ||
| }) | ||
| ``` | ||
|
|
||
| This avoids the awkward situation where a browser user sees raw JSON on an error page, or an API client receives HTML it cannot parse. | ||
|
|
||
| ## Where to Start | ||
|
|
||
| If your application uses the default error handler, start by adding a custom one that does two things: logs the real error and sends a generic message. That single change eliminates the most common information leakage vector. | ||
|
|
||
| Once that is in place, introduce a domain error type for the status codes and messages your handlers use most. You do not need to cover every case on day one. | ||
|
|
||
| ## Internal References | ||
|
|
||
| - [Error Handling Guide](/guide/error-handling) | ||
| - [Recover Middleware](/middleware/recover) | ||
| - [What's New](/whats_new) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.