Skip to content

Commit e2d13bd

Browse files
committed
feat(api): add Engine.SetNoRoute(h) for late-binding the SPA handler
WithNoRoute(h) at construction covers most cases, but SPA hosts often resolve their frontend FS (via embed.FS sub or fs.Sub) AFTER the Engine is constructed — e.g. lthn/desktop builds pkg/server first, then pkg/desktop reads its embedded dist/ and attaches the fallback. SetNoRoute lets the handler land late. Mirrors the existing late-binding pattern for groups (Engine.Register). TestSetNoRoute_Good_AttachesAfterConstruction covers the canonical late-bind flow.
1 parent 9d6d33e commit e2d13bd

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

go/api.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,22 @@ func (e *Engine) Serve(ctx context.Context) (
289289
return <-errCh
290290
}
291291

292+
// SetNoRoute attaches or replaces the fallback handler invoked when
293+
// no registered route matches the incoming request. Mirrors the
294+
// WithNoRoute Option but is callable after construction — useful
295+
// when the SPA handler is built later in the boot sequence than
296+
// the Engine (e.g. once the embedded frontend FS is resolved).
297+
// Pass nil to clear and restore gin's default 404.
298+
//
299+
// Example:
300+
//
301+
// e, _ := api.New()
302+
// // … later, once dist/ is mounted …
303+
// e.SetNoRoute(spaHandler)
304+
func (e *Engine) SetNoRoute(h gin.HandlerFunc) {
305+
e.noRouteHandler = h
306+
}
307+
292308
// build creates a configured Gin engine with recovery middleware,
293309
// user-supplied middleware, the health endpoint, and all registered route groups.
294310
func (e *Engine) build() *gin.Engine {

go/no_route_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,34 @@ func TestWithNoRoute_Bad_DoesNotShadowExplicitRoutes(t *testing.T) {
6060
}
6161
}
6262

63+
// TestSetNoRoute_Good_AttachesAfterConstruction verifies late binding —
64+
// SetNoRoute mirrors WithNoRoute but is callable after New() returns.
65+
// Pattern: SPA host knows its frontend FS only after the asset embed
66+
// resolves, which can land later than the Engine's construction.
67+
func TestSetNoRoute_Good_AttachesAfterConstruction(t *testing.T) {
68+
const payload = "LATE-BOUND-SPA"
69+
70+
e, err := New() // no WithNoRoute at construction time
71+
if err != nil {
72+
t.Fatalf("api.New: %v", err)
73+
}
74+
75+
e.SetNoRoute(func(c *gin.Context) {
76+
c.String(http.StatusOK, payload)
77+
})
78+
79+
w := httptest.NewRecorder()
80+
req := httptest.NewRequest(http.MethodGet, "/some/spa/route", nil)
81+
e.Handler().ServeHTTP(w, req)
82+
83+
if w.Code != http.StatusOK {
84+
t.Fatalf("status = %d, want 200", w.Code)
85+
}
86+
if got := w.Body.String(); got != payload {
87+
t.Fatalf("body = %q, want %q", got, payload)
88+
}
89+
}
90+
6391
// TestWithNoRoute_Ugly_NilStaysAsDefault404 verifies the degenerate path —
6492
// no NoRoute set means gin's default 404 surfaces unchanged. This protects
6593
// the contract that WithNoRoute is opt-in and unset Engines stay

0 commit comments

Comments
 (0)