Skip to content

fix(server): route OIDC in HTTP transport, graceful shutdown, validate env defaults - #1040

Merged
cristim merged 11 commits into
feat/multicloud-web-frontendfrom
fix/server-transport-config
Jun 7, 2026
Merged

fix(server): route OIDC in HTTP transport, graceful shutdown, validate env defaults#1040
cristim merged 11 commits into
feat/multicloud-web-frontendfrom
fix/server-transport-config

Conversation

@cristim

@cristim cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Closes fix(server): /oidc/.well-known/* not served in HTTP/container mode (only Lambda) — federation breaks on Cloud Run/Container Apps #1024 -- OIDC issuer endpoints (/oidc/.well-known/openid-configuration, /oidc/.well-known/jwks.json) were only wired in the Lambda transport; the HTTP/container transport (Cloud Run, Container Apps) had no registration so requests fell to the SPA file server and returned HTML or 404, breaking federated-credential trust chains. Fix: register handleOIDCHTTP at api.OIDCBasePath+"/" in CreateHTTPServer, ordered before the SPA handler (mirrors handleLambdaHTTPEvent, closes D1 transport-drift gap). Also fixes M2: reads app.staticDir instead of re-invoking staticDirFromEnv().
  • Closes fix(server): HTTP mode has no SIGTERM handling / server.Shutdown (connections + advisory locks leak on stop) #1025 -- StartHTTPServer blocked on ListenAndServe with no signal handling; SIGTERM (normal container stop) killed the process before deferred app.Close() could drain the DB pool and release session-pinned advisory locks. Fix: signal.NotifyContext(SIGINT, SIGTERM), goroutine for ListenAndServe, srv.Shutdown with 30s bounded timeout on signal.
  • Closes fix(config): validate DEFAULT_PAYMENT_OPTION / DEFAULT_RAMP_SCHEDULE at boundary (silently propagated as purchase default) #1026 -- DEFAULT_PAYMENT_OPTION and DEFAULT_RAMP_SCHEDULE env values were taken raw and passed to purchase.NewManager without validation despite ValidPaymentOptions/ValidRampScheduleTypes existing. A typo propagated silently as a system-wide purchase default. Fix: config.ValidatePaymentOptionEnv / config.ValidateRampScheduleEnv exported at config boundary; called from validateAppConfigEnvDefaults in NewApplicationFromDeps (fail-fast, consistent with scheduledauth/ADMIN_PASSWORD_SECRET). Also fixes M1: getEnvInt/getEnvFloat (app.go) and getTaskTimeout (cmd/server/main.go) now log WARNING on parse failure instead of silently swallowing it.

Changed files

File Change
internal/server/http.go Add handleOIDCHTTP, register at /oidc/, use app.staticDir, add graceful shutdown to StartHTTPServer
internal/server/app.go Add validateAppConfigEnvDefaults, call in NewApplicationFromDeps, fix getEnvInt/getEnvFloat to warn on bad parse
internal/config/validation.go Add ValidatePaymentOptionEnv / ValidateRampScheduleEnv exported boundary validators
cmd/server/main.go Fix getTaskTimeout to warn on bad parse
internal/server/http_test.go TestHTTPTransportServesOIDCEndpoints, TestHandleOIDCHTTP
internal/server/app_test.go TestNewApplicationFromDepsValidatesEnvDefaults, TestGetEnvIntLogsOnBadValue, TestGetEnvFloatLogsOnBadValue; fix existing test using canonical payment option spelling

Test plan

  • go build ./... -- clean
  • go vet ./internal/server/... ./internal/config/... ./cmd/server/... -- clean
  • go test ./internal/server/... ./internal/config/... ./cmd/server/... -- 912 passed, 0 failed
  • TestHTTPTransportServesOIDCEndpoints -- verifies OIDC paths return application/json, not HTML, in HTTP transport (pre-fix: would have served SPA fallback)
  • TestHandleOIDCHTTP -- verifies bridge handler returns JSON
  • TestNewApplicationFromDepsValidatesEnvDefaults -- verifies "AllUpfront" / "Immediate" (wrong-case typos) are rejected at startup with the env var name in the error
  • TestGetEnvIntLogsOnBadValue / TestGetEnvFloatLogsOnBadValue -- verify WARNING is logged on malformed env var
  • Graceful-shutdown full e2e (SIGTERM -> drain -> exit) is not feasible as a unit test without a live server; the wiring (signal.NotifyContext, goroutine, srv.Shutdown with bounded timeout) has been code-reviewed against the pattern

Summary by CodeRabbit

  • Bug Fixes

    • Configuration defaults for payment options and ramp schedules are now validated at startup with clear error messages for invalid values.
  • New Features

    • Server now handles shutdown signals gracefully (SIGINT/SIGTERM) for clean termination.
    • Improved OIDC endpoint routing in HTTP mode.
  • Improvements

    • Environment variable parsing now logs warnings when values cannot be parsed, improving debuggability.

Scope expanded

Added 7 commits implementing the remaining FOLD-1040 findings from report 04 (server/config) and report 06 (analytics). Tracked by issue #1067.

04-M5 + 04-N3 — runtime.IsLambda() used directly; isLambdaRuntime() wrapper removed

cmd/server/main.go was reading AWS_LAMBDA_RUNTIME_API directly, bypassing the runtime.IsLambda() helper that exists to keep the detection rule consistent. The isLambdaRuntime() wrapper in app.go had a "do not use" comment but was still called at 3 sites. Removed the wrapper and used runtime.IsLambda() everywhere.

04-L4 + 04-L5 + 04-N4 — health encode log; scheduled task path; unknown event error

  • handleHealthCheck now logs the json.Encode error for parity with handleScheduledHTTP.
  • handleScheduledHTTP replaced manual parts[2] indexing with strings.TrimPrefix.
  • Unknown Lambda event types now return a distinct "unrecognised Lambda event shape" error instead of silently routing to the scheduled handler.

04-L2 + 04-M6 — dead hasFileContent removed; unified static-file containment check

  • hasFileContent was dead code with a misleading comment. Removed.
  • spaHandler.ServeHTTP (HTTP path) and resolveStaticFilePath (Lambda path) used divergent containment checks. Unified by delegating spaHandler to resolveStaticFilePath. Fixed the prefix check to include os.PathSeparator (prevents sibling-dir confusion).

04-N1 — Version threaded directly via NewApplication; env round-trip removed

Both cmd/server and cmd/lambda pushed the build-time Version into os.Setenv("VERSION",...) and read it back. Changed NewApplication to accept a version parameter; "" falls back to the env for callers without a build-time constant.

04-M4 — Bearer-mode secret resolution error propagated at startup

When SCHEDULED_TASK_SECRET_NAME resolved but the Key Vault / Secrets Manager lookup failed, startup fell through to the misleading "bearer mode requires SCHEDULED_TASK_SECRET" error. resolveScheduledTaskSecret now returns (string, error); NewApplicationFromDeps loads the auth mode config first and propagates resolver errors in bearer mode with the actual cause.

04-M3 — migrationsTimeout/runMigrationsFunc moved to struct fields (parallel-safe)

Package-level migrationsTimeout and runMigrations vars required tests to avoid t.Parallel(). Moved to Application struct fields set by the constructor. Tests now pass a fake runner to runMigrationsBoundedWith() directly and can call t.Parallel().

06-M4 — Non-concurrent MV refresh in migration; error surfaced in API response

refresh_savings_materialized_views() uses CONCURRENTLY which is illegal inside a transaction. Migration 000003 called this function at schema-apply time. Replaced with plain REFRESH MATERIALIZED VIEW statements in the migration. Also added "views_error" to the handleRefreshAnalytics result map so refresh failures are visible in the API response, not only in server logs.

Also closes #1067

@cristim cristim added bug Something isn't working triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/few Limited audience effort/m Days type/bug Defect labels Jun 7, 2026
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 2 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8aaa33e-51d0-4aa8-bcb3-4acedb35bbb7

📥 Commits

Reviewing files that changed from the base of the PR and between 8037036 and e50df95.

📒 Files selected for processing (15)
  • cmd/lambda/main.go
  • cmd/lambda/main_test.go
  • cmd/server/main.go
  • internal/config/validation.go
  • internal/database/postgres/migrations/000003_analytics_partitions.up.sql
  • internal/server/app.go
  • internal/server/app_test.go
  • internal/server/handler.go
  • internal/server/health.go
  • internal/server/http.go
  • internal/server/http_test.go
  • internal/server/lambda.go
  • internal/server/lambda_test.go
  • internal/server/static.go
  • internal/server/static_test.go
📝 Walkthrough

Walkthrough

This PR hardens server startup robustness by validating payment and ramp-schedule configuration at application initialization, fixes feature parity between Lambda and HTTP transports for OIDC federation endpoints, and implements graceful shutdown on process termination to prevent connection and lock leaks.

Changes

Server startup robustness and HTTP transport alignment

Layer / File(s) Summary
Environment validation helper functions
internal/config/validation.go
ValidatePaymentOptionEnv and ValidateRampScheduleEnv validate non-empty environment values against allowed option sets, treating empty strings as valid (use built-in defaults) and returning formatted error messages listing allowed values on invalid input.
Config validation at application startup
internal/server/app.go
Introduces validateAppConfigEnvDefaults to fail fast on invalid DEFAULT_PAYMENT_OPTION or DEFAULT_RAMP_SCHEDULE during NewApplicationFromDeps, preventing misconfigured money-moving defaults from propagating silently into the system.
Configuration validation tests and fixtures
internal/server/app_test.go
Test fixture is updated to use canonical lowercase payment option value; new TestNewApplicationFromDepsValidatesEnvDefaults verifies startup rejects invalid defaults while accepting empty and valid values.
OIDC HTTP endpoint routing
internal/server/http.go
Adds OIDC request interception in CreateHTTPServer before static fallback via new handleOIDCHTTP bridge method, translating incoming HTTP requests into Lambda-compatible handler calls and returning 404 JSON for unrecognized OIDC paths, aligning HTTP transport behavior with Lambda.
OIDC HTTP endpoint coverage
internal/server/http_test.go
Tests confirm HTTP transport intercepts /oidc/.well-known/openid-configuration and /oidc/.well-known/jwks.json requests, returning application/json responses instead of SPA-fallback HTML or 404, even when no signer is configured.
Graceful HTTP shutdown on signals
internal/server/http.go
Rewrites StartHTTPServer to run ListenAndServe asynchronously and handle SIGINT/SIGTERM via signal.NotifyContext, performing graceful server.Shutdown with 30-second timeout to drain connections and deferred cleanup before process exit.
Environment variable parsing with warning logs
cmd/server/main.go, internal/server/app.go, internal/server/app_test.go
getTaskTimeout, getEnvInt, and getEnvFloat now emit WARNING logs when environment variables are set but fail to parse, instead of silent fallback; tests verify captured log output includes the env key name and log level.

Sequence Diagram

sequenceDiagram
  participant Server as HTTP Server
  participant Signal as OS Signal
  participant Handler as signal.NotifyContext
  participant ListenServe as ListenAndServe
  participant Shutdown as server.Shutdown
  participant AppClose as app.Close

  Server->>Handler: signal.NotifyContext(SIGINT, SIGTERM)
  Server->>ListenServe: ListenAndServe (goroutine)
  Signal-->>Handler: SIGINT/SIGTERM
  Handler->>Shutdown: ctx canceled
  Shutdown->>ListenServe: graceful drain (30s timeout)
  ListenServe-->>Shutdown: drain complete
  Shutdown->>AppClose: deferred cleanup runs
  AppClose-->>Server: connections closed, locks released
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

impact/many

Poem

🐰 Validation whispers at the gate,
OIDC routes no longer stray,
Graceful signals seal the fate,
No more leaks to ruin the day!
Config speaks before startup's too late. 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the three main changes: OIDC routing in HTTP transport, graceful shutdown implementation, and environment variable validation.
Linked Issues check ✅ Passed All objectives from #1024, #1025, and #1026 are met: OIDC routing added before static handler, graceful shutdown with signal handling implemented, and env defaults validated at startup with warning logs.
Out of Scope Changes check ✅ Passed All changes directly address the three linked issues; no out-of-scope modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/server-transport-config

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
internal/server/http_test.go (1)

419-420: 💤 Low value

Redundant assertion—already covered by line 414.

The assertion on line 414-415 already guarantees Content-Type starts with application/json. If we reach line 419, ct is already known to be JSON, so this condition will always pass.

Consider removing or simplifying:

♻️ Suggested simplification
-			// With no signer configured HandleOIDC returns 404 JSON (not 200);
-			// the important invariant is that it is *not* an SPA-served HTML 200.
-			testutil.AssertTrue(t, resp.StatusCode != http.StatusOK || strings.HasPrefix(ct, "application/json"),
-				"SPA fallback must not serve OIDC paths")
+			// With no signer configured HandleOIDC returns 404 JSON.
+			testutil.AssertEqual(t, http.StatusNotFound, resp.StatusCode)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/http_test.go` around lines 419 - 420, The assertion using
testutil.AssertTrue that checks "resp.StatusCode != http.StatusOK ||
strings.HasPrefix(ct, \"application/json\")" is redundant because the earlier
assertion already guarantees ct starts with "application/json"; remove this
assertion (the testutil.AssertTrue call referencing resp.StatusCode,
http.StatusOK, ct and strings.HasPrefix) or simplify it to only assert
resp.StatusCode != http.StatusOK so the SPA fallback test only enforces that
OIDC paths are not served with HTTP 200.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/server/http_test.go`:
- Around line 419-420: The assertion using testutil.AssertTrue that checks
"resp.StatusCode != http.StatusOK || strings.HasPrefix(ct,
\"application/json\")" is redundant because the earlier assertion already
guarantees ct starts with "application/json"; remove this assertion (the
testutil.AssertTrue call referencing resp.StatusCode, http.StatusOK, ct and
strings.HasPrefix) or simplify it to only assert resp.StatusCode !=
http.StatusOK so the SPA fallback test only enforces that OIDC paths are not
served with HTTP 200.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 369ba0d9-cd29-455a-91ff-35f8ff76db20

📥 Commits

Reviewing files that changed from the base of the PR and between cbdc4be and 8037036.

📒 Files selected for processing (6)
  • cmd/server/main.go
  • internal/config/validation.go
  • internal/server/app.go
  • internal/server/app_test.go
  • internal/server/http.go
  • internal/server/http_test.go

@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Addressed CR review findings:

  • http_test.go:419-420 redundant assertion (nitpick) -> fixed in aae88ca: replaced the tautological AssertTrue(StatusCode != 200 || HasPrefix(ct,"application/json")) (line 414 already guarantees the JSON content-type, so the OR could never fail) with an explicit AssertEqual(t, http.StatusNotFound, resp.StatusCode), which actually exercises the documented "no signer -> 404 JSON, not SPA-fallback 200" invariant.

All 294 tests in internal/server pass; gofmt/vet/build clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough explanation and the clean fix. The replacement assertion — AssertEqual(t, http.StatusNotFound, resp.StatusCode) — is strictly stronger than the tautological OR and directly documents the "no signer → 404 JSON, not SPA-fallback 200" contract. Kicking off a fresh review pass now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

FOLD-1040 additions (issue #1067)

Seven additional commits implementing the remaining findings from code-review report 04 (server/config) and report 06 (analytics).

Commit Finding(s) Change
e06b450 04-M5, 04-N3 Use runtime.IsLambda() directly in cmd/server/main.go; remove isLambdaRuntime() wrapper from app.go
8ba7b29 04-L4, 04-L5, 04-N4 Log health encode error; replace manual parts[2] with TrimPrefix; return distinct error for unknown Lambda event type
e9e8b0e 04-L2, 04-M6 Remove dead hasFileContent; unify static-file containment check between HTTP and Lambda paths with separator-aware prefix
d735051 04-N1 Thread Version via NewApplication(ctx, version) param; remove os.Setenv("VERSION",...) round-trip from both cmd entrypoints
576f098 04-M4 resolveScheduledTaskSecret returns (string, error); bearer-mode startup propagates resolver error with the actual cause
096943a 04-M3 Move migrationsTimeout/runMigrationsFunc from package globals onto Application struct fields; tests now use t.Parallel()
6b14b76 06-M4 Replace CONCURRENTLY call in migration with plain REFRESH; add views_error to refresh-analytics API response

Verification:

  • go build ./... -- clean
  • go vet ./internal/server/... ./cmd/... -- clean
  • go test ./internal/server/... ./cmd/... -- 1096 passed (up from 1094; 2 new regression tests added)

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough change table — the diff coverage looks solid across all seven findings. Highlights that stand out positively:

  • 04-N1 (Version via param, no os.Setenv round-trip) and 04-M3 (globals → struct fields + t.Parallel()) are meaningful structural improvements that reduce implicit state.
  • 04-M4 (resolveScheduledTaskSecret returning (string, error)) closes a silent-failure path that was a real reliability risk.
  • 06-M4 (drop CONCURRENTLY in the migration, surface views_error) is a good correctness fix — CONCURRENTLY in a transaction block is a Postgres error anyway.

Kicking off a fresh review pass now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added 10 commits June 7, 2026 11:23
…shutdown

Closes #1024: /oidc/.well-known/* was only wired in the Lambda transport
(handleLambdaHTTPEvent); the HTTP/container transport (Cloud Run, Container
Apps) had no OIDC registration so requests fell to the SPA file server and
returned HTML or 404 -- breaking federated-credential trust chains. Fix:
register handleOIDCHTTP at api.OIDCBasePath+"/" in CreateHTTPServer, placed
before the static/SPA handler so the ordering matches handleLambdaHTTPEvent
(D1 anti-drift). Also replace the double staticDirFromEnv() call with
app.staticDir (single source of truth, M2).

Closes #1025: StartHTTPServer blocked on ListenAndServe with no signal
handling, so SIGTERM (normal stop in Cloud Run/Container Apps/Fargate) killed
the process before deferred app.Close() could drain the DB pool and release
session-pinned advisory locks. Fix: wire signal.NotifyContext(SIGINT,SIGTERM),
run ListenAndServe in a goroutine, on signal call srv.Shutdown with a 30s
bounded timeout.

Regression tests added: TestHTTPTransportServesOIDCEndpoints confirms the HTTP
transport returns application/json (not HTML) for OIDC paths; TestHandleOIDCHTTP
verifies the bridge handler itself. Graceful-shutdown unit test is not feasible
without a live server -- wiring is exercised through code review of StartHTTPServer.
… startup boundary

Closes #1026: DEFAULT_PAYMENT_OPTION and DEFAULT_RAMP_SCHEDULE env values were
taken raw by LoadApplicationConfig and passed to purchase.NewManager without
validation, despite config.ValidPaymentOptions / config.ValidRampScheduleTypes
existing. A typo ("AllUpfront", "Immediate") silently became the system-wide
purchase default -- a money-moving misconfiguration caught only at AWS purchase
time (noisy, late) or never.

Fix: add config.ValidatePaymentOptionEnv / config.ValidateRampScheduleEnv
exported at the config package boundary, called from a new
validateAppConfigEnvDefaults helper in NewApplicationFromDeps. Invalid non-empty
values cause a startup error (consistent with the scheduledauth / ADMIN_PASSWORD_SECRET
fail-fast posture in the same file). Empty value remains valid.

Also fix M1: getEnvInt / getEnvFloat in app.go and getTaskTimeout in cmd/server/main.go
silently swallowed strconv parse errors, leaving operators who fat-fingered a tuning
env var believing their value was in effect. All three now log WARNING with the
key name and the default value being used.

Regression tests added: TestNewApplicationFromDepsValidatesEnvDefaults (invalid
payment option and ramp schedule each cause a startup error with the env var name
in the message); TestGetEnvIntLogsOnBadValue and TestGetEnvFloatLogsOnBadValue
confirm WARNING is emitted on parse failure. Existing TestNewApplicationFromDeps
updated to use canonical "all-upfront" (was "AllUpfront", now correctly rejected).
The previous assertion (resp.StatusCode != http.StatusOK ||
strings.HasPrefix(ct, "application/json")) was a tautology: line 414
already guarantees ct starts with application/json, so the OR could
never fail. Replace it with an explicit AssertEqual to http.StatusNotFound
so the test fails loudly if an OIDC path ever falls through to the SPA
fallback. Addresses CodeRabbit nitpick on http_test.go:419-420.
…daRuntime() wrapper

The isLambdaRuntime() wrapper existed to keep call sites unchanged during
an earlier refactor but its own comment said new code should call
runtime.IsLambda() directly (04-N3). Remove the wrapper and inline
runtime.IsLambda() at all three app.go call sites.

cmd/server/main.go read AWS_LAMBDA_RUNTIME_API directly despite the
runtime package existing to encapsulate exactly this detection rule
(04-M5). Replace with runtime.IsLambda() to keep the detection logic
consistent if the rule changes in future.

Part of #1067.
…rror on unknown event

04-L4: handleHealthCheck silently discarded the json.Encode return value
while handleScheduledHTTP already logged it. Log the error for parity; the
headers are already sent so the only recovery is a log entry.

04-L5: handleScheduledHTTP re-derived the task-type segment by splitting the
path and indexing parts[2], which requires a length guard and breaks if the
route prefix changes. Replace with strings.TrimPrefix against the known
prefix; reject empty or slash-containing results.

04-N4: unrecognised Lambda payloads were silently routed to the scheduled
handler, which then failed with "unknown scheduled task action" masking the
real root cause. Return a distinct "unrecognised Lambda event shape" error so
operators see the actual problem in the logs.

Regression test: TestHandleLambdaEvent_UnknownEventReturnsError.

Part of #1067.
…Content

04-M6: spaHandler.ServeHTTP (HTTP transport) and resolveStaticFilePath
(Lambda transport) implemented two different directory-containment checks.
spaHandler delegated containment to http.ServeFile's built-in protection
while resolveStaticFilePath used strings.HasPrefix(absFile, absDir) without
a trailing separator -- allowing a sibling directory sharing a name prefix
to pass the check. Unify by delegating spaHandler to resolveStaticFilePath
and fix the prefix check to include os.PathSeparator.

04-L2: hasFileContent was dead code. The doc comment claimed it was used
at startup to validate STATIC_DIR, but staticDirFromEnv does that work
and never called hasFileContent. Remove it and its three tests.

Regression test: TestResolveStaticFilePath_SiblingDirBlocked.

Part of #1067.
…env round-trip

Both cmd/server and cmd/lambda pushed the build-time Version into
os.Setenv("VERSION",...) so LoadApplicationConfig could read it back.
This round-trip was awkward (cmd/lambda even had a thread-safety caveat)
and unnecessary once the constructor accepts the value directly (04-N1).

Add a version parameter to NewApplication: when non-empty it overrides
the env-sourced value; "" falls back to os.Getenv("VERSION") for
callers that do not have a build-time constant. Both cmd entrypoints
now pass their ldflags-stamped Version directly.

Update TestInitApp_SetsVersion to reflect the new behaviour (VERSION
env var is intentionally not set by initApp).

Part of #1067.
04-M4: when SCHEDULED_TASK_SECRET_NAME is configured but the SecretResolver
(Key Vault, Secrets Manager) fails, resolveScheduledTaskSecret logged the
error and fell back to the empty SCHEDULED_TASK_SECRET env var. In bearer
mode buildScheduledAuth then raised "bearer mode requires SCHEDULED_TASK_SECRET"
-- pointing the operator at the wrong env var and hiding the real cause.

Fix:
- resolveScheduledTaskSecret now returns (string, error); the error is
  non-nil when a SecretName is configured and resolution fails.
- NewApplicationFromDeps loads the scheduledauth config before resolving
  the secret so it knows the mode. In bearer mode, a non-nil resolver
  error is propagated immediately with the original cause and the secret
  name in the message.
- buildScheduledAuth is replaced by buildScheduledAuthFromConfig which
  accepts the pre-loaded Config, avoiding a duplicate LoadConfig call.

Regression test: TestNewApplicationFromDeps_BearerModeSecretResolutionFails.

Part of #1067.
…tion struct fields

04-M3: migrationsTimeout and runMigrations were package-level mutable vars
used as test-injection points, with a "MUST NOT call t.Parallel()" comment
the compiler cannot enforce. A future contributor adding t.Parallel() to a
migration test would trigger a data race.

Fix:
- Add migrationsTimeout time.Duration and runMigrationsFunc fields to
  Application; set them in NewApplicationFromDeps.
- Add app.runMigrationsBounded() method that delegates to
  runMigrationsBoundedWith(), a pure function accepting a runner argument.
- Remove package-level migrationsTimeout var and runMigrations var.
- Update tests to pass a fake runner directly to runMigrationsBoundedWith
  and add t.Parallel() calls (now safe).

Regression test: TestEnsureDB_UsesInstanceMigrationsTimeout -- two
Application instances with different timeouts run in parallel.

Part of #1067.
…urface error in result

06-M4 (migration half): REFRESH MATERIALIZED VIEW CONCURRENTLY cannot run
inside a transaction block. Migration 000003 called
refresh_savings_materialized_views() which used CONCURRENTLY for all three
views -- this would fail at every fresh deploy. Replace with plain (non-
concurrent) REFRESH statements directly in the migration body. Reserve
CONCURRENTLY for the runtime scheduled-task path (refresh_savings_
materialized_views()) which runs outside any transaction.

06-M4 (server half): handleRefreshAnalytics only logged a view-refresh
failure as "Warning" with no way for the API caller or the operator reading
the scheduled-task response body to see the cause. Add "views_error" to
the result map so the error is visible in the response alongside the
existing server-side log.

Part of #1067.
CI pre-commit checks flagged two issues after the FOLD-1040 commits:

gofmt: trailing newline in static.go and misaligned struct fields in
app.go (migrationsTimeout/runMigrationsFunc). Fixed by gofmt -w.

Cyclomatic complexity > 10:
- resolveStaticFilePath (11): extract isPathContainedIn helper that
  encapsulates the separator-aware prefix check (04-M6). Reduces the
  compound &&-condition branch inside resolveStaticFilePath.
- NewApplicationFromDeps (12): extract initScheduledAuth helper that
  consolidates the scheduledauth.LoadConfig / resolveScheduledTaskSecret /
  buildScheduledAuthFromConfig / Warmup sequence added for 04-M4. The
  function reads as one call at the call site instead of four nested
  if/err branches.

Part of #1067.
@cristim
cristim force-pushed the fix/server-transport-config branch from 3407fd0 to e50df95 Compare June 7, 2026 18:24
@cristim

cristim commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Rebase onto feat/multicloud-web-frontend (post #1049)

Rebased fix/server-transport-config onto origin/feat/multicloud-web-frontend after #1049 (analytics historical snapshots) merged and caused a conflict in internal/server/app.go.

Conflict resolved: single conflict in LoadApplicationConfig() at line 264-269. HEAD had both isLambdaRuntime() and the new Analytics: LoadAnalyticsConfig() line added by #1049; the incoming commit (e06b450) replaced isLambdaRuntime() with runtime.IsLambda() but predated the Analytics field. Resolution: kept runtime.IsLambda() (the #1040 intent) and retained Analytics: LoadAnalyticsConfig() (the #1049 addition).

Verification: go build ./... clean, go vet ./internal/server/... ./cmd/... clean, go test ./internal/server/... ./internal/config/... ./cmd/... 1667 passed.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

@cristim The rebase conflict resolution looks correct — runtime.IsLambda() (from e06b450) and Analytics: LoadAnalyticsConfig() (from #1049) are orthogonal fields in LoadApplicationConfig(), so keeping both is the right merge. Clean build + 1667 passing tests confirms the resolution is sound. Triggering a fresh review pass now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly GCP service converters (`convertGCPRecommendation` in memorystore/client.go and cloudstorage/client.go), `rec.Term` must be derived from `params.Term` with a `"1yr"` default — not hardcoded to `"1yr"`. Without this, 3-year callers always emit 1-year commitments even though the downstream `termYears` derivation from `rec.Term` is correct. Fixed in PR `#1047` commit c6280c390 (F2 for memorystore, F4 for cloudstorage). Regression tests: `TestConvertGCPRecommendation_PropagatesTermFromParams` and `TestCloudStorageConvertGCPRecommendation_PropagatesTermFromParams`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:45:38.040Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/cloudsql/client.go`, `getSQLPricing` had the same per-hour/term-total unit mismatch as memorystore and cloudstorage: `commitmentPrice` (per-hour SKU rate) was passed directly to savings math that expected a term total, producing ~99.99% savings, and `HourlyRate` was set to `per-hour / hoursInTerm` (near-zero). Fixed in PR `#1047` commit 20590c6b4 (issue `#1078` folded in): `commitmentPriceTerm := commitmentPrice * hoursInTerm`; `HourlyRate = commitmentPrice` (raw per-hour). `convertGCPRecommendation` also hardcoded `rec.Term = "1yr"`, ignoring `params.Term`; fixed with same defaulting pattern as memorystore/cloudstorage. Regression tests: `TestGetSQLPricing_CommitmentPriceIsTermTotal` and `TestCloudSQLConvertGCPRecommendation_PropagatesTermFromParams`. PaymentOption was already correct (`if paymentOption == "" { paymentOption = "monthly" }`).

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-07T17:38:19.416Z
Learning: In LeanerCloud/CUDly `providers/gcp/services/computeengine/client.go`, `convertGCPRecommendation` must force `paymentOption = "monthly"` unconditionally (logging a WARN when a non-monthly value such as "upfront", "all-upfront", or "partial-upfront" is supplied) because GCP CUDs are monthly-only and any non-monthly value passed through would be a silent misconfiguration. Fixed in PR `#1047` commit c6280c390 (F1). Regression test: `TestConvertGCPRecommendation_NonMonthlyPaymentOptionForcedToMonthly`.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-06-06T06:43:53.770Z
Learning: In LeanerCloud/CUDly PR `#995` (issue `#950` ownership gate), the Dashboard "Upcoming purchases" widget (`dashboard.ts`) has two Cancel entry points (card-level and modal) that both call `api.deletePlannedPurchase`. The `UpcomingPurchase` response DTO must include `CreatedByUserID *string` (omitempty, mirroring `PlannedPurchase`) so the frontend can gate these buttons via a `canCancelUpcomingPurchase` helper that mirrors `canManageScheduledPurchase` on the Plans page. Without this, the backend correctly 403s non-owners but the UI has no gate and the UX is broken. Regressions: `frontend/src/__tests__/dashboard-ownership-950.test.ts` (6 tests) and `TestHandler_getUpcomingPurchases_PropagatesCreatedByUserID`. Fixed in commit 94326f6b9.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 69c5a0c into feat/multicloud-web-frontend Jun 7, 2026
4 checks passed
@cristim
cristim deleted the fix/server-transport-config branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working effort/m Days impact/few Limited audience priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant