Skip to content

feat: add SetNotFoundHandler and SetMethodNotAllowedHandler for custom 404 and 405 responses - #56

Open
qm012 wants to merge 4 commits into
mainfrom
handle404_405
Open

feat: add SetNotFoundHandler and SetMethodNotAllowedHandler for custom 404 and 405 responses#56
qm012 wants to merge 4 commits into
mainfrom
handle404_405

Conversation

@qm012

@qm012 qm012 commented Aug 27, 2026

Copy link
Copy Markdown
Owner

related: #38

Adds two setters to App for customizing error responses:

  • SetNotFoundHandler — served when a request matches no registered pattern (404)
  • SetMethodNotAllowedHandler — served when a path matches a pattern but its method does not (405)

Both are optional: leaving them unset keeps the mux's default responses, and
ServeHTTP takes a fast path straight to the mux in that case.

Why

http.ServeMux has no public hook for its 404/405 handlers (see
golang/go#65648), so apps could not customize
error pages or JSON error bodies without bypassing the mux entirely.

Usage

JSON API errors — return a structured body instead of the mux's plain text:

app := sim.NewApp()
app.Get("/users/{id}", userHandler)

app.SetNotFoundHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusNotFound)
    _, _ = io.WriteString(w, `{"code":404,"message":"resource not found"}`)
}))

app.SetMethodNotAllowedHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Allow is already set to the methods the mux computed; keep it.
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusMethodNotAllowed)
    _, _ = io.WriteString(w, `{"code":405,"message":"method not allowed"}`)
}))

_ = app.Run(context.Background(), ":8080")

A single handler can cover both cases if only the status differs:

errorPage := func(code int) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        w.WriteHeader(code)
        fmt.Fprintf(w, "<h1>%d</h1><p><a href=\"/\">Back home</a></p>", code)
    })
}
app.SetNotFoundHandler(errorPage(http.StatusNotFound))
app.SetMethodNotAllowedHandler(errorPage(http.StatusMethodNotAllowed))

Notes:

  • Handlers set on the root app are inherited by groups created with App.Group, so sub-routers share the same error pages.
  • Setting only one handler leaves the other on the mux default.
  • The 405 handler receives the response with Allow pre-populated per RFC 9110; it may override or delete it.

@qm012 qm012 self-assigned this Aug 27, 2026
@qm012 qm012 added the enhancement New feature or request label Aug 27, 2026
Comment thread app.go
// does not populate r.pat/r.matches, so r.Pattern and
// r.PathValue would be broken for wildcard patterns.
a.mux.ServeHTTP(w, r)
return

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With custom error handlers set, matched requests now match twice (mux.Handler + mux.ServeHTTP), since Handler doesn't populate r.Pattern/PathValue. I assume that's acceptable as it's slow-path only — the alternative (a WriteHeader-intercepting wrapper) looks like more complexity than it saves. Worth confirming.

@github-actions

Copy link
Copy Markdown

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/qm012/sim 90.51% (+1.09%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/qm012/sim/app.go 76.60% (+8.95%) 94 (+26) 72 (+26) 22 👍

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/qm012/sim/app_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant