Skip to content

fix(oauth): surface backend outage instead of opening browser to 504 (#1985)#2147

Merged
senamakel merged 4 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/oauth-backend-health-banner-1985
May 20, 2026
Merged

fix(oauth): surface backend outage instead of opening browser to 504 (#1985)#2147
senamakel merged 4 commits into
tinyhumansai:mainfrom
CodeGhost21:fix/oauth-backend-health-banner-1985

Conversation

@CodeGhost21
Copy link
Copy Markdown
Contributor

@CodeGhost21 CodeGhost21 commented May 18, 2026

Summary

Fixes #1985 (also refs #1998) — when api.tinyhumans.ai returns a Cloudflare 504 Gateway Time-out on OAuth login, the desktop app currently opens the system browser to a broken page and then silently re-enables the button after 90s with no user feedback. There is no way to tell service down from user cancelled. The same UX gap also makes #1998 (users in China hitting NXDOMAIN / 530 / 503 on the auth + API hosts) look like silent failure. This PR adds:

  1. A short /health pre-flight on click that blocks openUrl() and surfaces a clear "OpenHuman cloud sign-in is temporarily unavailable" banner when the backend is unreachable.
  2. A background probe on browser-return (focus / visibilitychange / 90s timeout reset) so an outage that starts mid-flow also gets attributed correctly instead of looking like a user cancellation.

What changed

New: app/src/services/backendHealth.ts

  • checkBackendHealthy({ timeoutMs?, fetchImpl? }) → Promise<BackendHealthResult>.
  • Uses an AbortController with a 6s default budget (4s for the inline click pre-flight) against GET /health.
  • Discriminated result: reason ∈ 'timeout' | 'network' | 'http-5xx' | 'resolve-failure'.
  • Treats 4xx as healthy — the goal is to catch edge/origin outages (Cloudflare 5xx, DNS, offline), not endpoint existence. A /health that happens to be 404 in some environment still proves the edge is reachable.
  • No new dependencies; uses native fetch + AbortController.

app/src/components/oauth/OAuthProviderButton.tsx

  • Pre-flight on click — before openUrl(loginUrl), run checkBackendHealthy({ timeoutMs: 4_000 }). Unhealthy → show banner, skip the browser open, re-enable the button.
  • Post-return probe — after focus / visibilitychange / 90s safety timeout fires and no deep-link round-trip is in flight, fire a non-blocking checkBackendHealthy() to attribute the "didn't complete" signal. Healthy → silent reset (same as today, user cancelled). Unhealthy → same banner.
  • A browserOpenedRef so a pre-flight failure doesn't trigger a redundant probe via the focus listener that gets armed while isLoading=true.

Tests

  • New app/src/services/__tests__/backendHealth.test.ts — 7 cases: 200 happy / 404 (still healthy) / 504 → reason http-5xx / AbortError → reason timeout / TypeError → reason network / getBackendUrl() rejection → reason resolve-failure / default-timeout regression guard.
  • Updated app/src/components/oauth/__tests__/OAuthProviderButton.test.tsx — 6 new cases: parametrized pre-flight failure (each reason) blocks openUrl and shows banner; post-return probe shows banner when backend is unhealthy; stays silent when backend is healthy (user cancelled); 90s-timeout probe surfaces banner; no double-probe when pre-flight already blocked the browser open.
  • Updated app/test/OAuthDiscord.test.tsx, OAuthGitHub.test.tsx, OAuthLoginSection.test.tsx, OAuthTwitter.test.tsx — mock services/backendHealth with a default healthy result so their existing happy-path flows aren't blocked by the new pre-flight.

Why this approach

  • Fail fast on click — pre-flight catches Cloudflare 504 / DNS / offline before the user is sent to the broken provider page. With a healthy backend, overhead is ~100-200ms; the 4s click budget is well under the typical browser-open latency.
  • Catch outages that start mid-flow — the background probe on browser-return covers the case where the OAuth endpoint was up at click time but went down before the user finished.
  • Distinguish cancel from outage — without the probe, "OAuth didn't complete" is the only signal, and it fires both for user cancellation and backend down. The probe lets us only surface the banner for genuine outages, so we don't badger users who deliberately cancelled.
  • Same fix covers the China-region symptom from Unable to sign in from China - auth server unreachable (auth.tinyhumans.ai DNS NXDOMAIN, app.openhuman.ai returns 530) #1998reason: 'resolve-failure' / 'network' / 'http-5xx' all funnel into the same banner, so DNS NXDOMAIN on auth.tinyhumans.ai and 530/503 on app.openhuman.ai / api.tinyhumans.ai now produce a clear user-visible signal instead of a hung sign-in button.
  • No architecture change — small, contained fix specifically for the reported failure mode. The reporter's broader suggestion of full offline / local-only mode is out of scope here.

Out of scope

Test plan

  • pnpm debug unit — full suite green locally (268 files / 2622 tests passed, including the 4 OAuth files that previously failed before the mock update).
  • pnpm lint — no new errors/warnings introduced by the new files.
  • pnpm compile (tsc --noEmit) — clean.

Manual smoke (recommended before merge)

These exercise the user-visible behavior and cannot be covered by unit tests alone:

  1. With api.tinyhumans.ai reachable → click Google → system browser opens normally to OAuth (no perceptible added latency).
  2. With backend deliberately unreachable (e.g. VITE_BACKEND_URL=http://127.0.0.1:1) → click Google → red banner appears, browser does not open.
  3. Backend healthy at click, then taken down mid-OAuth → on browser-return, banner appears within ~6s instead of the user waiting through the 90s safety timer.

Refs #1985, #1998.

Summary by CodeRabbit

  • New Features

    • OAuth login now performs a backend-health pre-flight before opening the browser and re-checks on return, showing a temporary “backend unavailable” banner when the service is unreachable.
    • A short safety timeout ensures users see the unavailable message if the backend remains unresponsive.
  • Tests

    • Expanded test coverage for backend health scenarios: pre-flight blocking, post-return handling, timeout behavior, and error classifications (network/timeout/resolve failure).

Review Change Stack

…inyhumansai#1985)

Click → GET /health pre-flight (4s budget) → block openUrl and show
"OpenHuman cloud sign-in is temporarily unavailable" banner on Cloudflare
5xx, DNS, abort, or offline. After a healthy click, a background probe
on focus/visibility/90s-timeout return still surfaces the banner if the
backend went down mid-flow.

Before this change, a 504 from Cloudflare during OAuth (tinyhumansai#1985) sent the
user into the system browser to an error page and back to a silently
re-enabling button. There was no way to tell "service down" from "user
cancelled" — both paths just reset and waited.

The new services/backendHealth service is independent of services/backendUrl
and treats only 5xx + network/abort failures as unhealthy; a 404 still
proves the edge is reachable. Tests cover happy / 4xx / 5xx / AbortError /
TypeError / resolve-failure plus the OAuthProviderButton wiring for both
pre-flight and post-return probe paths.
@CodeGhost21 CodeGhost21 requested a review from a team May 18, 2026 19:56
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a010a1a-862a-435e-97f9-be97d940cd35

📥 Commits

Reviewing files that changed from the base of the PR and between b3cf1a8 and 2d1bc59.

📒 Files selected for processing (7)
  • app/src/components/oauth/OAuthProviderButton.tsx
  • app/src/components/oauth/__tests__/OAuthProviderButton.test.tsx
  • app/src/services/backendHealth.ts
  • app/test/OAuthDiscord.test.tsx
  • app/test/OAuthGitHub.test.tsx
  • app/test/OAuthLoginSection.test.tsx
  • app/test/OAuthTwitter.test.tsx

📝 Walkthrough

Walkthrough

Adds a backend /health probe service and integrates pre-flight and return-path health checks into the OAuth provider button; tests and OAuth test harnesses are updated to mock and validate probe outcomes.

Changes

OAuth Backend Health Pre-Flight and Return-Path Probing

Layer / File(s) Summary
Backend health probe service and tests
app/src/services/backendHealth.ts, app/src/services/__tests__/backendHealth.test.ts
Adds BACKEND_HEALTH_TIMEOUT_MS, types, and checkBackendHealthy() that fetches /health with abort timeout; classifies results (healthy, http-5xx, timeout, network, resolve-failure) and records latency; tests cover all outcomes.
OAuth button pre-flight and return-path health checks
app/src/components/oauth/OAuthProviderButton.tsx
Integrates pre-flight health probe before opening the system browser, tracks browserOpenedRef, and probes backend on return paths (focus, visibility, timeout); sets BACKEND_UNAVAILABLE_MESSAGE and clears loading when unhealthy.
OAuthProviderButton unit tests and regression tests
app/src/components/oauth/__tests__/OAuthProviderButton.test.tsx
Mocks checkBackendHealthy (default healthy), replaces microtask drains with looped drains, and adds regression tests for pre-flight blocking, post-return probing behavior, banner visibility, and 90s safety timeout.
Integration test harness mocks for OAuth
app/test/OAuthDiscord.test.tsx, app/test/OAuthGitHub.test.tsx, app/test/OAuthLoginSection.test.tsx, app/test/OAuthTwitter.test.tsx
Adds mockCheckBackendHealthy hoisted mocks and module mocks for ../src/services/backendHealth so OAuth integration tests default to a healthy backend probe.

Sequence Diagram

sequenceDiagram
  participant User
  participant OAuthProviderButton
  participant checkBackendHealthy
  participant getBackendUrl
  participant FetchAPI
  participant SystemBrowser
  User->>OAuthProviderButton: click provider button
  OAuthProviderButton->>checkBackendHealthy: call()
  checkBackendHealthy->>getBackendUrl: resolve backend URL
  alt resolved
    checkBackendHealthy->>FetchAPI: GET {backendUrl}/health (AbortController)
    FetchAPI->>checkBackendHealthy: response (status / error)
    alt healthy
      checkBackendHealthy-->>OAuthProviderButton: healthy
      OAuthProviderButton->>SystemBrowser: open browser (set browserOpenedRef)
      SystemBrowser-->>OAuthProviderButton: user returns (focus/visibility)
      OAuthProviderButton->>checkBackendHealthy: probe on return
    else unhealthy
      checkBackendHealthy-->>OAuthProviderButton: unhealthy -> set BACKEND_UNAVAILABLE_MESSAGE
    end
  else resolve-failure
    getBackendUrl-->>checkBackendHealthy: throw -> checkBackendHealthy returns resolve-failure
    checkBackendHealthy-->>OAuthProviderButton: unhealthy -> set BACKEND_UNAVAILABLE_MESSAGE
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • senamakel

Poem

🐰 I sniff the network, tiny paws in flight,

A pre-flight probe before the browser's light.
If servers nap, I drum the warning bell,
If they're awake, the OAuth dance goes well.
Hooray—health checks keep the login bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding backend health checking to prevent opening the browser on OAuth failures, matching the PR's primary objective of surfacing outage messages instead of opening to error pages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
app/src/components/oauth/OAuthProviderButton.tsx (1)

84-101: ⚡ Quick win

Switch new console.* diagnostics to the project’s namespaced debug logger.

The newly added OAuth probe/preflight logs should align with the repo’s frontend logging convention.

As per coding guidelines: “Follow existing patterns for debug logging (e.g. the debug npm package with a namespace per area) plus dev-only detail where useful.”

Also applies to: 173-177

🤖 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 `@app/src/components/oauth/OAuthProviderButton.tsx` around lines 84 - 101,
Replace the ad-hoc console.* calls in the OAuthProviderButton probe flow with
the project's namespaced debug logger: import and instantiate the module-scoped
debug logger (using the repo convention and a namespace like
'oauth:oauth-button' or the existing namespace used elsewhere) and use it for
the three probe branches (unhealthy log, healthy log, and thrown error) inside
the OAuthProviderButton component (the code that references provider.id, label,
setStartupError, BACKEND_UNAVAILABLE_MESSAGE and the checkBackendHealthy
promise). Ensure the unhealthy log includes the result.reason, latencyMs, and
status (same fields as before) via the debug logger, the healthy branch logs
status and latencyMs, and the catch branch logs the error via debug; also update
the other similar console.* occurrences around the second probe block (lines
173-177) to use the same namespaced debug logger so all frontend OAuth probe
diagnostics follow the project convention.
app/src/services/backendHealth.ts (1)

35-36: ⚡ Quick win

Use namespaced debug logging instead of console.* in frontend TS code.

New logging here should follow the repo’s namespaced debug pattern with dev-only detail gating.

As per coding guidelines: “Follow existing patterns for debug logging (e.g. the debug npm package with a namespace per area) plus dev-only detail where useful.”

Also applies to: 52-64

🤖 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 `@app/src/services/backendHealth.ts` around lines 35 - 36, Replace console.*
calls with the repo’s namespaced debug logger: import the debug factory (e.g.
from 'debug') and create a namespace for this module like const debug =
createDebug('app:backend-health') (or use the existing debug helper if present),
then replace console.debug and other console.* usages in backendHealth.ts (the
branch around the console.debug('[backend-health] could not resolve backend
URL', err) and the similar blocks in lines 52-64) with debug(...) calls. Gate
verbose/error detail behind a dev-only check (e.g. if (process.env.NODE_ENV ===
'development') debug('... %O', err) for stack/obj details) while keeping concise
messages in non-dev; ensure the new debug import and namespace are used
consistently across all replaced console statements in this file.
app/src/services/__tests__/backendHealth.test.ts (1)

6-6: ⚡ Quick win

Make getBackendUrl mocking explicit in this test file.

vi.mocked(getBackendUrl) assumes the module is already mocked elsewhere. Add a local vi.mock('../backendUrl', ...) to avoid hidden setup coupling and keep this suite deterministic.

Proposed patch
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { BACKEND_HEALTH_TIMEOUT_MS, checkBackendHealthy } from '../backendHealth';
 import { getBackendUrl } from '../backendUrl';
+
+vi.mock('../backendUrl', () => ({ getBackendUrl: vi.fn() }));
As per coding guidelines: “Keep tests deterministic: avoid real network calls, time-sensitive flakes, or hidden global state.”

Also applies to: 16-18

🤖 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 `@app/src/services/__tests__/backendHealth.test.ts` at line 6, Add an explicit
vi.mock for the module that exports getBackendUrl at the top of this test file
and replace the implicit vi.mocked(getBackendUrl) usage with a mocked function
from that explicit mock; specifically, call vi.mock(...) to return an object
with getBackendUrl: vi.fn(), then obtain mockedGetBackendUrl =
vi.mocked(getBackendUrl) and set its mockImplementation/mockResolvedValue as
needed (also update the other uses of mockedGetBackendUrl around the existing
assertions where it’s referenced). Ensure the mock is local to this suite so
tests remain deterministic.
🤖 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.

Inline comments:
In `@app/src/components/oauth/OAuthProviderButton.tsx`:
- Around line 171-181: The preflight call to checkBackendHealthy can throw and
leave the component in a loading state; wrap the await checkBackendHealthy({
timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS }) in a try/catch, log the error
(including provider.id), call setStartupError(BACKEND_UNAVAILABLE_MESSAGE),
ensure setIsLoading(false) is called in the catch, and return early so the
button doesn't stay disabled; keep the existing healthy-branch logic intact.

In `@app/src/services/backendHealth.ts`:
- Around line 43-48: Replace the raw fetchImpl call used to probe
`${backendUrl}/health` with an in-process core relay invocation using
invoke('core_rpc_relay', ...) so the health check goes through the local core
socket (not CORS/network). Locate the fetchImpl usage in this file (the call
that passes method: 'GET', cache: 'no-store', credentials: 'omit', signal:
controller.signal) and change it to call the core relay with the equivalent HTTP
method and path ('/health'), forward the abort signal/timeout behavior from
controller.signal, and handle the response/parsing the same way; remove reliance
on backendUrl and fetchImpl for this status probe so it always uses the
in-process core socket. Ensure error handling and timeouts remain consistent
with the previous logic in this module (use the same controller and response
handling code paths).

---

Nitpick comments:
In `@app/src/components/oauth/OAuthProviderButton.tsx`:
- Around line 84-101: Replace the ad-hoc console.* calls in the
OAuthProviderButton probe flow with the project's namespaced debug logger:
import and instantiate the module-scoped debug logger (using the repo convention
and a namespace like 'oauth:oauth-button' or the existing namespace used
elsewhere) and use it for the three probe branches (unhealthy log, healthy log,
and thrown error) inside the OAuthProviderButton component (the code that
references provider.id, label, setStartupError, BACKEND_UNAVAILABLE_MESSAGE and
the checkBackendHealthy promise). Ensure the unhealthy log includes the
result.reason, latencyMs, and status (same fields as before) via the debug
logger, the healthy branch logs status and latencyMs, and the catch branch logs
the error via debug; also update the other similar console.* occurrences around
the second probe block (lines 173-177) to use the same namespaced debug logger
so all frontend OAuth probe diagnostics follow the project convention.

In `@app/src/services/__tests__/backendHealth.test.ts`:
- Line 6: Add an explicit vi.mock for the module that exports getBackendUrl at
the top of this test file and replace the implicit vi.mocked(getBackendUrl)
usage with a mocked function from that explicit mock; specifically, call
vi.mock(...) to return an object with getBackendUrl: vi.fn(), then obtain
mockedGetBackendUrl = vi.mocked(getBackendUrl) and set its
mockImplementation/mockResolvedValue as needed (also update the other uses of
mockedGetBackendUrl around the existing assertions where it’s referenced).
Ensure the mock is local to this suite so tests remain deterministic.

In `@app/src/services/backendHealth.ts`:
- Around line 35-36: Replace console.* calls with the repo’s namespaced debug
logger: import the debug factory (e.g. from 'debug') and create a namespace for
this module like const debug = createDebug('app:backend-health') (or use the
existing debug helper if present), then replace console.debug and other
console.* usages in backendHealth.ts (the branch around the
console.debug('[backend-health] could not resolve backend URL', err) and the
similar blocks in lines 52-64) with debug(...) calls. Gate verbose/error detail
behind a dev-only check (e.g. if (process.env.NODE_ENV === 'development')
debug('... %O', err) for stack/obj details) while keeping concise messages in
non-dev; ensure the new debug import and namespace are used consistently across
all replaced console statements in this file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c011ad3-9c60-4186-84b2-fb9b67af47ed

📥 Commits

Reviewing files that changed from the base of the PR and between 0b053c5 and ee17a91.

📒 Files selected for processing (8)
  • app/src/components/oauth/OAuthProviderButton.tsx
  • app/src/components/oauth/__tests__/OAuthProviderButton.test.tsx
  • app/src/services/__tests__/backendHealth.test.ts
  • app/src/services/backendHealth.ts
  • app/test/OAuthDiscord.test.tsx
  • app/test/OAuthGitHub.test.tsx
  • app/test/OAuthLoginSection.test.tsx
  • app/test/OAuthTwitter.test.tsx

Comment on lines +171 to +181
const preflight = await checkBackendHealthy({ timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS });
if (!preflight.healthy) {
console.warn(`[oauth-button][${provider.id}] preflight → backend unhealthy`, {
reason: preflight.reason,
latencyMs: preflight.latencyMs,
status: 'status' in preflight ? preflight.status : undefined,
});
setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
setIsLoading(false);
return;
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard pre-flight probe with try/catch to avoid a stuck loading state on unexpected throws.

If checkBackendHealthy() throws unexpectedly, isLoading is never reset and the button can remain disabled.

Proposed patch
- const preflight = await checkBackendHealthy({ timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS });
- if (!preflight.healthy) {
-   console.warn(`[oauth-button][${provider.id}] preflight → backend unhealthy`, {
-     reason: preflight.reason,
-     latencyMs: preflight.latencyMs,
-     status: 'status' in preflight ? preflight.status : undefined,
-   });
-   setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
-   setIsLoading(false);
-   return;
- }
+ let preflight;
+ try {
+   preflight = await checkBackendHealthy({ timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS });
+ } catch (error) {
+   setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
+   setIsLoading(false);
+   return;
+ }
+ if (!preflight.healthy) {
+   console.warn(`[oauth-button][${provider.id}] preflight → backend unhealthy`, {
+     reason: preflight.reason,
+     latencyMs: preflight.latencyMs,
+     status: 'status' in preflight ? preflight.status : undefined,
+   });
+   setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
+   setIsLoading(false);
+   return;
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const preflight = await checkBackendHealthy({ timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS });
if (!preflight.healthy) {
console.warn(`[oauth-button][${provider.id}] preflight → backend unhealthy`, {
reason: preflight.reason,
latencyMs: preflight.latencyMs,
status: 'status' in preflight ? preflight.status : undefined,
});
setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
setIsLoading(false);
return;
}
let preflight;
try {
preflight = await checkBackendHealthy({ timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS });
} catch (error) {
setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
setIsLoading(false);
return;
}
if (!preflight.healthy) {
console.warn(`[oauth-button][${provider.id}] preflight → backend unhealthy`, {
reason: preflight.reason,
latencyMs: preflight.latencyMs,
status: 'status' in preflight ? preflight.status : undefined,
});
setStartupError(BACKEND_UNAVAILABLE_MESSAGE);
setIsLoading(false);
return;
}
🤖 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 `@app/src/components/oauth/OAuthProviderButton.tsx` around lines 171 - 181, The
preflight call to checkBackendHealthy can throw and leave the component in a
loading state; wrap the await checkBackendHealthy({ timeoutMs:
OAUTH_PREFLIGHT_TIMEOUT_MS }) in a try/catch, log the error (including
provider.id), call setStartupError(BACKEND_UNAVAILABLE_MESSAGE), ensure
setIsLoading(false) is called in the catch, and return early so the button
doesn't stay disabled; keep the existing healthy-branch logic intact.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping — checkBackendHealthy() is contract-bound to never throw. Both code paths that could reject are caught internally:

  • getBackendUrl() rejection → caught at backendHealth.ts:34 → returns {healthy: false, reason: 'resolve-failure'}
  • fetch() rejection (AbortError / TypeError / etc.) → caught at backendHealth.ts:63 → returns {healthy: false, reason: 'timeout' | 'network'}

Adding an outer try/catch in the click handler would be defensive code for an unreachable state. The resolve-failure reason case is also covered by a unit test (backendHealth.test.ts) so the contract is regression-protected.

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.

@CodeGhost21 Fair point — the AI summary of backendHealth.ts confirms both rejection paths are converted to result objects internally, so the "never throws" behaviour holds for the current implementation.

One lightweight safeguard worth considering: a JSDoc annotation on checkBackendHealthy would make the contract explicit for future maintainers (and TypeScript-aware editors) so a refactor inside the service doesn't silently invalidate the no-try/catch assumption at call sites:

/**
 * Probes the backend `/health` endpoint.
 *
 * **Never throws.** All network, timeout, and URL-resolution failures are
 * caught internally and surfaced as `{ healthy: false, reason: … }` results.
 */
export async function checkBackendHealthy(options: CheckOptions = {}): Promise<BackendHealthResult> {

That way the contract is documented at the source of truth rather than implicitly relied upon by callers. Up to you whether it's worth adding.

(ノ◕ヮ◕)ノ*:・゚✧


🧠 Learnings used
Learnt from: graycyrus
Repo: tinyhumansai/openhuman PR: 167
File: app/src/components/OnboardingOverlay.tsx:30-39
Timestamp: 2026-04-01T13:52:17.936Z
Learning: In React components, do not perform synchronous `setState` (or other state-updating calls) directly inside `useEffect` bodies. The lint rule `react-hooks/set-state-in-effect` (enforced in this codebase) disallows patterns such as resetting state flags at the top of an effect before returning. Refactor to avoid calling state setters in the effect; prefer deriving state from props/render, using refs, or restructuring logic so state updates occur outside `useEffect`.

Comment on lines +43 to +48
const response = await fetchImpl(`${backendUrl}/health`, {
method: 'GET',
cache: 'no-store',
credentials: 'omit',
signal: controller.signal,
});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace raw fetch() with in-process core relay for backend health probing.

This fetch call violates the frontend service-call policy and reintroduces CORS/network-path variability for a core status check.

Suggested direction
- const response = await fetchImpl(`${backendUrl}/health`, {
-   method: 'GET',
-   cache: 'no-store',
-   credentials: 'omit',
-   signal: controller.signal,
- });
+ // Route through invoke('core_rpc_relay', ...) via the existing core client path
+ // used for service-status style checks, then map response status into
+ // BackendHealthResult the same way.

As per coding guidelines: “Always call the in-process core via invoke('core_rpc_relay', ...) — never use raw fetch() (CORS preflight) or callCoreRpc() for service-status calls when the socket may not be connected yet.”

🤖 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 `@app/src/services/backendHealth.ts` around lines 43 - 48, Replace the raw
fetchImpl call used to probe `${backendUrl}/health` with an in-process core
relay invocation using invoke('core_rpc_relay', ...) so the health check goes
through the local core socket (not CORS/network). Locate the fetchImpl usage in
this file (the call that passes method: 'GET', cache: 'no-store', credentials:
'omit', signal: controller.signal) and change it to call the core relay with the
equivalent HTTP method and path ('/health'), forward the abort signal/timeout
behavior from controller.signal, and handle the response/parsing the same way;
remove reliance on backendUrl and fetchImpl for this status probe so it always
uses the in-process core socket. Ensure error handling and timeouts remain
consistent with the previous logic in this module (use the same controller and
response handling code paths).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping — core_rpc_relay / callCoreRpc proxies to the local in-process core at http://127.0.0.1:<port>/rpc (see coreRpcClient.ts:335-378 — it builds a JSON-RPC payload, fetches getCoreRpcUrl(), and authenticates with the local-only OPENHUMAN_CORE_TOKEN bearer). It is not a generic HTTP egress proxy and it cannot reach api.tinyhumans.ai.

Issue #1985 is specifically about the external backend api.tinyhumans.ai returning Cloudflare 504. Probing the local core's /health would only confirm the local core is alive — which says nothing about whether the external host is reachable. The pre-flight has to hit the external host directly to detect the reported failure mode.

CORS — verified with curl -H "Origin: tauri://localhost" https://api.tinyhumans.ai/health -D -:

HTTP/2 200
access-control-allow-origin: *
access-control-allow-credentials: true
content-type: application/json; charset=utf-8

so the direct fetch from the WebView is fully permitted; no CORS workaround is needed.

If we want to abstract this through Rust later (e.g. for offline scenarios), the right primitive would be a new dedicated Tauri command (like the existing exchange_token / openhuman.config_resolve_api_url), not core_rpc_relay which is RPC-specific.

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.

@CodeGhost21, you're right — I was wrong on this one. core_rpc_relay / callCoreRpc is strictly a local JSON-RPC tunnel to 127.0.0.1:<port> and has no path to api.tinyhumans.ai, so routing the external health probe through it would completely defeat the purpose. The CORS evidence (access-control-allow-origin: *) also closes the loop on why a direct fetch() from the WebView is safe here. The note about a future dedicated Tauri command being the right abstraction layer (rather than core_rpc_relay) is a good one to keep in mind if this ever needs a Rust-side egress path.

I'll resolve my comment. Would you like me to go ahead and do that now?


✏️ Learnings added
Learnt from: CodeGhost21
Repo: tinyhumansai/openhuman PR: 2147
File: app/src/services/backendHealth.ts:43-48
Timestamp: 2026-05-18T20:17:22.924Z
Learning: In the OpenHuman desktop app (Tauri, TypeScript/React), `core_rpc_relay` / `callCoreRpc` (defined in `app/src/services/coreRpcClient.ts` around lines 335-378) is an in-process RPC tunnel that proxies JSON-RPC payloads to the local core at `http://127.0.0.1:<port>/rpc` using a local `OPENHUMAN_CORE_TOKEN` bearer. It is NOT a generic HTTP egress proxy and cannot reach external services such as `api.tinyhumans.ai`. Never suggest routing external health/status checks through `core_rpc_relay`.

Learnt from: CodeGhost21
Repo: tinyhumansai/openhuman PR: 2147
File: app/src/services/backendHealth.ts:43-48
Timestamp: 2026-05-18T20:17:22.924Z
Learning: If a Rust-side abstraction for external HTTP egress is ever needed in OpenHuman (e.g., for offline/proxy scenarios), the correct pattern is a new dedicated Tauri command (similar to existing commands `exchange_token` or `openhuman.config_resolve_api_url`), not `core_rpc_relay`, which is RPC-specific.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Per review on tinyhumansai#2147: replace the implicit reliance on the global
services/backendUrl mock from app/src/test/setup.ts with an explicit
local vi.mock so the suite controls getBackendUrl behavior end-to-end,
including the rejection branch the 'resolve-failure' case asserts.
@CodeGhost21
Copy link
Copy Markdown
Contributor Author

Re: review feedback — verification summary

Verified each suggestion against the codebase before acting.

Applied (1 of 5):

  • Nitpick — explicit local vi.mock('../backendUrl', …) in backendHealth.test.ts — small isolation win; the suite no longer relies on the global mock at app/src/test/setup.ts:172. Commit b3cf1a88.

Skipped, with reasoning (see inline replies for the two "Potential issue" threads):

  • try/catch around await checkBackendHealthy(...) in the click handler — the function is contract-bound to never throw. Both rejection paths (getBackendUrl() and fetch) are caught internally and converted to {healthy: false, reason}. There is also a unit test (resolve-failure case) protecting that contract. An outer try/catch would be defensive code for an unreachable state. (replied inline)
  • Replace fetch with invoke('core_rpc_relay', …)core_rpc_relay is the local in-process JSON-RPC relay (http://127.0.0.1:<port>/rpc, bearer-authenticated with OPENHUMAN_CORE_TOKEN). It cannot reach api.tinyhumans.ai and probing the local core's /health would not detect the Cloudflare 504 from issue Bug: 504 Gateway Timeout on api.tinyhumans.ai during OAuth login (Google/GitHub/Twitter) #1985. Also confirmed api.tinyhumans.ai/health returns access-control-allow-origin: *, so the direct fetch works from the WebView without CORS workarounds. (replied inline)
  • Namespaced debug logger in OAuthProviderButton.tsx — the file already uses console.debug / warn / error ubiquitously (8 pre-existing calls in handlers I didn't touch). Migrating only the new additions would create inconsistency; migrating all is out of scope for the Bug: 504 Gateway Timeout on api.tinyhumans.ai during OAuth login (Google/GitHub/Twitter) #1985 fix. The new code follows the file's existing convention.
  • Namespaced debug logger in backendHealth.ts — services in app/src/services/ (analytics.ts, meetCallService.ts, daemonHealthService.ts, rewardsApi.ts) all use console.*. The new file follows the dominant convention in that directory.

Suite still green: pnpm debug unit OAuth → 5 files / 101 tests passing; pnpm compile clean; targeted lint clean.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Solid work — clean approach to a real UX pain point. Pre-flight + post-return probes are well-structured, discriminated result type keeps callers honest, and the test coverage is thorough (13 new test cases across service + component). Both #1985 and #1998 are addressed correctly. Two minor nits below, neither blocking.

// (issue #1985) instead of sending the user into a system browser that
// lands on a gateway-error page with no path back into the app.
const preflight = await checkBackendHealthy({ timeoutMs: OAUTH_PREFLIGHT_TIMEOUT_MS });
if (!preflight.healthy) {
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.

[minor] getBackendUrl() is resolved twice on the happy path — once inside checkBackendHealthy() to build the /health URL, and again here to build the login URL. Not a correctness issue, but if getBackendUrl() does any async work (Tauri command invoke, etc.) it's a redundant round-trip on every click.

One option: have checkBackendHealthy accept an optional pre-resolved URL, or return the resolved URL in the healthy result so you can reuse it here.

if (skipDuringDeepLink('focus')) return;
console.debug(`[oauth-button][${provider.id}] window focus → reset isLoading`);
reset();
probeBackendOnReturn('focus');
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.

[minor] focus and visibilitychange can fire back-to-back in the same event-loop turn when the user switches back to the app (macOS does this). Both handlers call reset() then probeBackendOnReturn(), so you can get two concurrent probes. Harmless (second probe sets the same state), but a simple probed flag on the ref would deduplicate:

if (!browserOpenedRef.current) return;
browserOpenedRef.current = false; // consume the flag

That way the first handler to fire claims the probe and the second is a no-op.

senamakel added 2 commits May 19, 2026 19:57
Addresses two minor maintainer (@graycyrus) review comments and one
CodeRabbit follow-up nit on PR tinyhumansai#2147:

- focus + visibilitychange can fire back-to-back on macOS when the user
  returns from the system browser. probeBackendOnReturn now consumes
  browserOpenedRef on first invocation so the second event is a no-op
  instead of triggering a redundant concurrent probe.
- The click handler used to call getBackendUrl() twice on the happy
  path (once inside checkBackendHealthy, once to build the loginUrl).
  The healthy result now carries the resolved backendUrl so the click
  handler can reuse it — saving a redundant Tauri IPC round-trip per
  click.
- Documented the "never throws" contract on checkBackendHealthy via
  JSDoc so future maintainers don't accidentally invalidate the
  no-try/catch assumption at call sites.

Tests updated to reflect that getBackendUrl is no longer called
directly from the OAuth click handler (the mocked checkBackendHealthy
owns it). The four OAuth integration test harnesses now mirror the
real checkBackendHealthy contract — a mockGetBackendUrl rejection
becomes a resolve-failure result instead of an unhandled throw.
@senamakel senamakel merged commit 9c14c13 into tinyhumansai:main May 20, 2026
24 of 27 checks passed
mtkik pushed a commit to mtkik/openhuman-meet that referenced this pull request May 21, 2026
CodeGhost21 added a commit to CodeGhost21/openhuman that referenced this pull request May 22, 2026
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: 504 Gateway Timeout on api.tinyhumans.ai during OAuth login (Google/GitHub/Twitter)

3 participants