Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pkg/gofr/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@ func (a *App) startMetricsServer(wg *sync.WaitGroup) {
}

// startHTTPServer starts the HTTP server if registered.
//
// If nothing marked the HTTP server for startup — no route was registered and
// no public/ directory exists — the server is skipped silently. That silence is
// a trap for an app that registered readiness checks but no routes: it starts
// metrics-only and stays unreachable, with /.well-known/health never served and
// no log line saying why. Warn when exactly that mismatch is detected, so the
// operator sees the cause instead of a connection refused.
func (a *App) startHTTPServer(wg *sync.WaitGroup) {
if a.httpRegistered {
wg.Add(1)
Expand All @@ -212,6 +219,14 @@ func (a *App) startHTTPServer(wg *sync.WaitGroup) {

s.run(a.container)
}(a.httpServer)

return
}

if len(a.readinessChecks) > 0 {
a.Logger().Warnf("readiness: %d app check(s) registered, but the HTTP server will not start — "+
"no route was registered and no public/ directory exists, so /.well-known/health is unreachable. "+
"Register at least one HTTP route.", len(a.readinessChecks))
}
}

Expand Down
42 changes: 42 additions & 0 deletions pkg/gofr/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"strconv"
"sync"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -160,6 +161,47 @@ func TestApp_Run_waitsForShutdown(t *testing.T) {
assert.NotContains(t, out.String(), shutdownRaced)
}

// TestApp_startHTTPServer_warnsWhenReadinessChecksCannotBeServed pins the warning for the
// metrics-only startup: an app that registers readiness checks but no HTTP routes skips the HTTP
// server without a word, and /.well-known/health is unreachable for no logged reason.
func TestApp_startHTTPServer_warnsWhenReadinessChecksCannotBeServed(t *testing.T) {
runStart := func(setup func(*App)) string {
// The app must be created INSIDE the capture: the container logger stores
// os.Stdout at construction, so an app built before the swap would log
// past the pipe and the capture would come back empty.
return testutil.StdoutOutputForFunc(func() {
app := New()

// New() marks the HTTP server for startup whenever the working directory
// happens to hold a static directory, so pin the state under test instead
// of inheriting whatever the CI/workspace checkout looks like.
app.httpRegistered = false

setup(app)

var wg sync.WaitGroup

app.startHTTPServer(&wg)
wg.Wait()
})
}

t.Run("readiness checks registered but no routes", func(t *testing.T) {
out := runStart(func(app *App) {
app.AddReadinessCheck(func(*Context) error { return nil })
})

assert.Contains(t, out, "readiness:", "the metrics-only startup must say why health is unreachable:\n%s", out)
assert.Contains(t, out, "the HTTP server will not start", "the warning must name the consequence:\n%s", out)
})

t.Run("no readiness checks and no routes stays silent", func(t *testing.T) {
out := runStart(func(*App) {})

assert.NotContains(t, out, "the HTTP server will not start", "an app without readiness checks has nothing to explain:\n%s", out)
})
}

// TestShutdownHelperProcess is the app under test for TestApp_Run_waitsForShutdown, run as its own
// process so the SIGTERM it is sent reaches nothing else. It skips unless the parent asked for it.
func TestShutdownHelperProcess(t *testing.T) {
Expand Down