Skip to content

fix(cli): wheels reload exits non-zero and reports the real verdict instead of false success - #3139

Merged
bpamiri merged 1 commit into
developfrom
peter/issue-3059-cli-reload-honesty
Jun 12, 2026
Merged

fix(cli): wheels reload exits non-zero and reports the real verdict instead of false success#3139
bpamiri merged 1 commit into
developfrom
peter/issue-3059-cli-reload-honesty

Conversation

@bpamiri

@bpamiri bpamiri commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

wheels reload printed Application reloaded successfully. whenever the HTTP exchange completed — it never read the status code. Against an app whose ?reload=true 500s (the #3053 Adobe regression) or with a wrong reload password (the framework serves the page normally, no restart), the CLI claimed success while nothing reloaded.

Verify-first: still reproduced on develop @ bb98ffecdreload() (Module.cfc) called makeHttpRequest() (which reads conn.getResponseCode() then discards it, and follows redirects) and unconditionally printed the green success line. The new failure-path specs were run red-first against unmodified develop: reload() against a stub returning 500 completed and printed success (toThrow failed), proving the defect live.

The 302-vs-200 contract

The framework's reload gate restarts the app and then location()-redirects (public/Application.cfc :: $handleRestartAppRequest), so a successful reload is always a 302. A wrong password falls through to normal page serving (200, or 404 without a root route). Verified live against the lucee7 docker harness (develop framework, reloadPassword="smokepw"):

Request (no -L) Status
warm app, ?reload=true&password=wrongpw 200 (page served, no restart)
warm app, ?reload=true&password=smokepw 302 (restart + redirect)
cold app, any password 302 (gate fires on cold start — and the reload genuinely happens, so reporting success is honest)

Changes (cli/lucli/Module.cfc)

  • reload() requests with redirects off and judges the raw status via a new $evaluateReloadResponse(statusCode) helper:
    • 3xx → success, output unchanged (acceptance: success path unchanged on Lucee 7)
    • 2xx (incl. 404 page render) → "Reload was not triggered … check the reload password", red
    • 4xx/5xx → "Reload failed: the server returned HTTP N", red
    • failures (and connection-level errors) print-then-throw Wheels.ReloadFailed per the fix(cli): upgrade-check exit code, breaker-scan coverage, ArgSpec positional gaps #2941 exit-code convention, so wheels reload && … CI gates work
  • makeHttpRequest() now delegates to a status-carrying variant makeHttpRequestWithStatus(url, followRedirects=true) — the other ~12 call sites keep their exact body-only, redirect-following behavior (the bot-triage "shared helper" trade-off resolved without disturbing callers)
  • The interactive console's /reload applies the same verdict, printing red instead of throwing (same false-success line, interactive context)
  • $evaluateReloadResponse is public only for specs; the structural $-prefix sweep in mcpHiddenTools() keeps it off the MCP surface

Coordination with #3062 (empty-password semantics, branch a2): based on develop, no dependency. If a2 lands "empty = disabled", an empty-password ?reload=true will render 200 and this CLI correctly reports "not triggered" + exits non-zero, with the existing WHEELS_RELOAD_PASSWORD hint.

Tests (CLI suite, lucee7 docker harness)

New in ReloadCommandSpec (+ cli/lucli/tests/StubHttpServer.cfc, a raw-socket fixed-status HTTP stub — com.sun.net.httpserver is unreachable from Lucee's OSGi classloader):

  • unit: $evaluateReloadResponse for 302 (success), 200 (failure, password hint), 404, 500
  • integration (per the issue's "stub server returning 500" acceptance): real reload() against the stub on an ephemeral port via the temp project's lucee.json500 → throws Wheels.ReloadFailed, 200 → throws, 302 → succeeds quietly

Evidence:

  • Red (unmodified develop logic): throws … returns 500 -> Failed: The incoming function did not throw… ✔ defect repro
  • Green (this branch): ReloadCommandSpec 9 pass / 0 fail / 0 error
  • Full CLI suite: 933 pass / 1 fail / 2 errors; stash-baseline on clean develop in the same container: 926 pass / 1 fail / 2 errors — the 3 artifacts are identical and pre-existing harness noise (SshClientSpec/SshPoolSpec: docker: command not found inside the container; ServerCommandsSpec "reload endpoint responds": the harness stages reloadPassword="" while the spec hardcodes password=wheels)

No vendor/wheels changes; core suite untouched.

Fixes #3059

🤖 Generated with Claude Code

…nstead of false success

reload() treated any completed HTTP exchange as success — it never read
the status code, so a 500 from the reload endpoint (the #3053 Adobe
regression) or a wrong-password page render both printed 'Application
reloaded successfully.' while nothing reloaded.

The framework's reload gate restarts the app then location()-redirects,
so a successful reload is always a 302. The CLI now requests with
redirects OFF and judges the raw status via $evaluateReloadResponse():
3xx = success (output unchanged), 2xx = not triggered (wrong password
hint), 4xx/5xx = endpoint error — failures print red then throw
Wheels.ReloadFailed per the #2941 exit-code convention. The console's
/reload applies the same verdict, printing red instead of throwing.

makeHttpRequest() now delegates to a status-carrying variant
(makeHttpRequestWithStatus) so the other call sites keep their exact
body-only behavior.

Specs: unit coverage of the verdict helper plus integration tests that
drive the real reload() against a raw-socket fixed-status HTTP stub
(StubHttpServer — com.sun.net.httpserver is unreachable from Lucee's
OSGi classloader) on an ephemeral port: 500 -> throws, 200 -> throws,
302 -> succeeds quietly.

Fixes #3059

Signed-off-by: Peter Amiri <peter@alurium.com>
@github-actions github-actions Bot added the docs label Jun 12, 2026

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer

TL;DR: This PR makes wheels reload read the HTTP status of the ?reload=true request and report honestly — 3xx (the framework's restart redirect) is success, 2xx/404 means the reload gate fell through (wrong password), 4xx/5xx is an endpoint error, and failures print red then throw Wheels.ReloadFailed for a non-zero exit. The core change is correct and well-evidenced; I verified the 302 contract, the MCP-surface claim, and the error-stream handling against the actual code. Verdict: comment — no blocking findings, one user-facing docs page now describes superseded behavior and should be updated (fine as a fast follow-up).

Verified (claims checked against the code, not just the PR body)

  • The 302 contract is real. public/Application.cfc::$handleRestartAppRequest (lines 473–474) does applicationStop() then location(url=..., addToken=false) — a successful reload always answers with a redirect. The wrong-password fall-through to normal page serving is visible in the gate at public/Application.cfc:273-296. Keeping redirects off in the CLI (makeHttpRequestWithStatus(reloadUrl, false), cli/lucli/Module.cfc:847) is the right call — following the redirect would collapse a real reload into the same 200 a wrong password produces.
  • $evaluateReloadResponse won't leak as an MCP tool. The PR's claim about the "structural $-prefix sweep" checks out: mcpHiddenTools() (cli/lucli/Module.cfc:199-217) reflects over getMetaData(this).functions and hides every public $-prefixed function. The public-for-specs access is the documented cli/CLAUDE.md carve-out ("must be public only when they need to be reachable as mixins/specs").
  • 4xx/5xx responses don't throw out of the HTTP helper. makeHttpRequestWithStatus already routes error statuses through conn.getErrorStream() with a null guard (cli/lucli/Module.cfc:7003-7010), so a 500 reaches the verdict helper as a status code rather than detouring through the connection-error catch. The other ~12 makeHttpRequest() call sites keep their exact prior behavior (body-only, redirects followed — setInstanceFollowRedirects(true) matches the HttpURLConnection default).
  • The new throw doesn't break internal flows. No internal caller of reload() exists (only CLI/MCP dispatch and the spec), and print-then-throw for non-zero exit has prior art in Wheels.TestsFailed (cli/lucli/Module.cfc:5341) and Wheels.ServerNotRunning (cli/lucli/Module.cfc:6731).
  • Tests are the right shape. Unit coverage of all four verdict classes plus integration tests that drive the real reload() against the raw-socket stub per the issue's acceptance criterion, with red-first evidence in the PR body. ReloadCommandSpec extends wheels.wheelstest.system.BaseSpec (the base used by all 23 specs in cli/lucli/tests/specs/commands/), ##3059 in describe strings is correctly escaped, and the stub's lifecycle is guarded (stop() in finally, threadJoin with timeout, per-connection setSoTimeout so a silent port probe can't wedge the accept loop).
  • Commit & changelog. Header is fix(cli): … at 92 chars with a why-focused body and a Signed-off-by: matching the author. Changelog fragment changelog.d/3059-cli-reload-false-success.fixed.md follows the fragment system (no direct CHANGELOG.md edit).

Docs

  • web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/dev-server.mdx:115 now describes superseded behavior. It reads: "On failure — most often a password mismatch — it prints Failed to reload: <message> and hints at setting WHEELS_RELOAD_PASSWORD…". After this PR, a password mismatch prints the new "Reload was not triggered: the server served the page normally (HTTP 200)…" message and the command exits non-zero; Failed to reload: <message> is now only the connection-error path. Suggest updating that paragraph to describe the three outcomes (302 success / 2xx-404 not-triggered with password hint / 4xx-5xx endpoint error) and the non-zero exit, since wheels reload && … gating is exactly what users will want to know about. Fine as a follow-up PR if you prefer to keep this one CLI-only.
  • Minor: the PR body doesn't use the repo PR template's feature-completeness checklist (.github/pull_request_template.md). The body covers the same ground honestly (tests, changelog, verification evidence), so this is purely a consistency nit.

No findings under Correctness, Conventions, Cross-engine (the CLI runs on the bundled Lucee only, per cli/CLAUDE.md), Tests, Commits, or Security.

@bpamiri
bpamiri merged commit 25dc794 into develop Jun 12, 2026
7 checks passed
@bpamiri
bpamiri deleted the peter/issue-3059-cli-reload-honesty branch June 12, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wheels reload reports "Application reloaded successfully." when the HTTP reload request fails (4xx/5xx)

1 participant