Skip to content

fix(app): normalize cloud core RPC URLs#2480

Merged
M3gA-Mind merged 4 commits into
tinyhumansai:mainfrom
YUHAO-corn:yuhao/fix-remote-core-cloudflare-2467
May 22, 2026
Merged

fix(app): normalize cloud core RPC URLs#2480
M3gA-Mind merged 4 commits into
tinyhumansai:mainfrom
YUHAO-corn:yuhao/fix-remote-core-cloudflare-2467

Conversation

@YUHAO-corn
Copy link
Copy Markdown
Contributor

@YUHAO-corn YUHAO-corn commented May 22, 2026

Summary

  • Normalize cloud core URLs so users can paste a core base URL like https://example.trycloudflare.com and still reach the JSON-RPC endpoint.
  • Apply the same normalization in the cloud-mode picker, persisted URL reads/writes, restored core mode state, and direct RPC probing.
  • Add regression coverage for Cloudflare-style base URLs, existing /rpc URLs, and previously persisted base URLs.

Problem

  • Users connecting the desktop client to a self-hosted core through Cloudflare Tunnel may paste the tunnel base URL instead of the /rpc endpoint.
  • The core root is reachable, but JSON-RPC calls belong on /rpc; using the base URL can make the connection flow fail even though the tunnel itself is healthy.
  • The issue report surfaced this as a 405 during remote-core connection setup.

Solution

  • Extend normalizeRpcUrl to append /rpc when the input URL has no path, while preserving existing /rpc URLs and non-root paths.
  • Reuse normalizeRpcUrl across BootCheckGate, coreRpcClient, configPersistence, and coreModeSlice so test connection, boot check, cached URL resolution, and localStorage restoration all agree.
  • Keep existing HTTP restrictions unchanged: public cloud URLs still require HTTPS, while local/private HTTP hosts remain allowed.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage >= 80% — focused Vitest coverage was added for the changed URL normalization paths; CI will enforce the merged diff-coverage gate.
  • Coverage matrix updated — N/A: behaviour-only cloud URL normalization fix; no feature matrix row added/removed/renamed.
  • All affected feature IDs from the matrix are listed in the PR description under ## Related — N/A: no coverage-matrix feature ID touched.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces (docs/RELEASE-MANUAL-SMOKE.md) — N/A: no release-cut smoke checklist surface changed.
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Runtime/platform: desktop/web app cloud-core connection setup and RPC URL resolution.
  • Compatibility: existing stored /rpc URLs continue to resolve unchanged; previously stored base URLs now self-heal on read.
  • Security: public HTTP cloud URLs are still rejected; no auth behavior or token storage behavior changes.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: yuhao/fix-remote-core-cloudflare-2467
  • Commit SHA: 5e95aeed8a97acee5823d73b6dc8e92f04af00fb

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: pnpm --dir app exec vitest run --config test/vitest.config.ts src/services/__tests__/coreRpcClient.test.ts src/utils/__tests__/configPersistence.test.ts src/store/coreModeSlice.test.ts src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx — 200 passed
  • Rust fmt/check (if changed): N/A: no Rust source changes; app format gate still ran Rust format checks.
  • Tauri fmt/check (if changed): N/A: no Tauri shell source changes; app format gate still ran Tauri Rust format checks.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: cloud core base URLs with no path are normalized to /rpc.
  • User-visible effect: users can paste a Cloudflare Tunnel base URL into the cloud runtime picker without manually appending /rpc.

Parity Contract

  • Legacy behavior preserved: existing /rpc URLs, auth token handling, RPC POST envelopes, and public-HTTP rejection behavior are unchanged.
  • Guard/fallback/dispatch parity checks: focused tests cover picker continuation, test connection, cached URL resolution, persisted URL reads/writes, and core-mode localStorage restoration.

Duplicate / Superseded PR Handling

Summary by CodeRabbit

  • Bug Fixes

    • Consistently normalize cloud RPC URLs: trims input, handles trailing slashes, and ensures the /rpc endpoint across input, storage, retrieval, and connection probes.
    • Safer RPC logging: credentials/query/hash are redacted for logged URLs.
  • Tests

    • Expanded coverage for URL normalization across connection flows, storage/readback, and boot checks.
  • Localization

    • Added German translations for subconscious and MCP server/settings UI strings.

Review Change Stack

@YUHAO-corn YUHAO-corn requested a review from a team May 22, 2026 04:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 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: e2d54c86-2a39-46eb-a167-380b0a81c955

📥 Commits

Reviewing files that changed from the base of the PR and between b189e2f and a89c207.

📒 Files selected for processing (4)
  • app/src/services/coreRpcClient.ts
  • app/src/utils/__tests__/configPersistence.test.ts
  • app/src/utils/configPersistence.ts
  • app/src/utils/redactRpcUrlForLog.ts
✅ Files skipped from review due to trivial changes (1)
  • app/src/utils/redactRpcUrlForLog.ts

📝 Walkthrough

Walkthrough

Normalize RPC URLs everywhere: persistence, client resolution, state hydration, and user input validation now rewrite base URLs (including trailing‑slash Cloudflare domains) to consistently target the JSON‑RPC /rpc endpoint before use or storage.

Changes

Cloud RPC URL Normalization

Layer / File(s) Summary
RPC URL normalization function and tests
app/src/utils/configPersistence.ts, app/src/utils/__tests__/configPersistence.test.ts
normalizeRpcUrl rewrites empty/root paths to /rpc, preserves query/hash, and has tests for trailing‑slash Cloudflare-style domains; module re-exports the redaction helper.
Redaction utility
app/src/utils/redactRpcUrlForLog.ts, app/src/utils/__tests__/configPersistence.test.ts
Adds redactRpcUrlForLog to strip credentials/query/hash for logging; tests assert redaction and invalid‑URL sentinel.
Persistence read/write normalization
app/src/utils/configPersistence.ts
storeRpcUrl, getStoredRpcUrl, and peekStoredRpcUrl normalize URLs on write/read and log redacted normalized values.
RPC client URL resolution and probe normalization
app/src/services/coreRpcClient.ts
coreRpcClient applies normalizeRpcUrl across web/Tauri/invoke/fallback paths and normalizes the probe URL in testCoreRpcConnection() before fetching.
RPC client tests updated for normalization
app/src/services/__tests__/coreRpcClient.test.ts
Tests inject normalizeMockRpcUrl into mocked configPersistence and assert normalized /rpc URLs and token-resolution behavior.
Core mode state hydration with normalization
app/src/store/coreModeSlice.ts, app/src/store/coreModeSlice.test.ts
deriveInitialMode() normalizes persisted cloud url when reconstructing cloud mode from localStorage; tests assert trailing‑slash base URLs become /rpc while tokens are preserved.
User input validation with URL normalization
app/src/components/BootCheckGate/BootCheckGate.tsx, app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx
BootCheckGate trims and normalizes user‑entered cloud URLs before parsing/validation; new tests assert both picker and test‑connection flows target /rpc.
German i18n additions
app/src/lib/i18n/chunks/de-3.ts, app/src/lib/i18n/chunks/de-5.ts
Added translation keys for subconscious UI and MCP server settings UI text.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2179: Touches coreRpcClient.testCoreRpcConnection() path; overlaps on probe behavior and timeouts/abort handling.

Suggested reviewers

  • senamakel

Poem

🐰 I trimmed the slashes, I nudged the RPC track,
From cloudbase to /rpc there's no turning back.
I hid the secrets, kept logs tidy and neat,
Hop—connections now land where server and client meet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 'fix(app): normalize cloud core RPC URLs' clearly and concisely describes the main change: normalizing cloud core RPC URLs to handle Cloudflare-style base URLs.
Linked Issues check ✅ Passed The PR fully addresses issue #2467: normalizes cloud RPC URLs by appending /rpc for base URLs without a path, enabling desktop client connection to Core via Cloudflare Tunnel. All coding requirements from the linked issue are met.
Out of Scope Changes check ✅ Passed All changes directly support URL normalization across BootCheckGate, coreRpcClient, configPersistence, and coreModeSlice. German translation additions (de-3.ts, de-5.ts) and new redactRpcUrlForLog utility are supporting changes that align with the PR objective.

✏️ 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.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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 22, 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: 1

🧹 Nitpick comments (1)
app/src/utils/configPersistence.ts (1)

87-89: ⚡ Quick win

Use the frontend debug helper and redact the URL.

This new log bypasses the repo’s namespaced debug pattern, and logging the full user-entered URL can leak embedded credentials or query tokens. Please switch to the shared debug logger and log a redacted value instead.

As per coding guidelines, "Use namespaced debug function in TypeScript frontend with dev-only detail logging." and "Never log secrets, raw JWTs, API keys, or full PII — redact or omit sensitive fields."

🤖 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/utils/configPersistence.ts` around lines 87 - 89, Replace the
console.debug call in the block that stores the RPC URL (around normalizeRpcUrl
and localStorage.setItem using RPC_URL_STORAGE_KEY) with the repo’s shared
frontend debug helper (the namespaced debug import used across the frontend),
and log a redacted form of the URL instead of the raw value; specifically, strip
any userinfo (user:pass@) and remove query string parameters from the normalized
value before passing it to the debug logger (e.g., debug('configPersistence:
Stored RPC URL: %s', redactedUrl)), ensuring you import the shared debug helper
the file already uses elsewhere and do not emit the full URL or any tokens.
🤖 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/utils/configPersistence.ts`:
- Around line 185-194: The helper currently trims trailing slashes from the
whole input (variable normalized) before parsing, which strips meaningful
search/hash values (e.g., https://host?next=/); change the flow to parse first
with new URL(input.trim()), then operate on parsed.pathname (remove trailing
slashes only from parsed.pathname) and rebuild the returned string using
parsed.origin + (adjusted pathname or '/rpc' logic) + parsed.search +
parsed.hash; update the code paths that reference normalized/parsed accordingly
and add regression tests covering inputs like "https://host?next=/" and
"https://host/#/" to ensure search/hash are preserved.

---

Nitpick comments:
In `@app/src/utils/configPersistence.ts`:
- Around line 87-89: Replace the console.debug call in the block that stores the
RPC URL (around normalizeRpcUrl and localStorage.setItem using
RPC_URL_STORAGE_KEY) with the repo’s shared frontend debug helper (the
namespaced debug import used across the frontend), and log a redacted form of
the URL instead of the raw value; specifically, strip any userinfo (user:pass@)
and remove query string parameters from the normalized value before passing it
to the debug logger (e.g., debug('configPersistence: Stored RPC URL: %s',
redactedUrl)), ensuring you import the shared debug helper the file already uses
elsewhere and do not emit the full URL or any tokens.
🪄 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: e1d072a0-91d7-47ef-9bc2-0cd496b91af6

📥 Commits

Reviewing files that changed from the base of the PR and between 7fe3dd0 and 5e95aee.

📒 Files selected for processing (8)
  • app/src/components/BootCheckGate/BootCheckGate.tsx
  • app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx
  • app/src/services/__tests__/coreRpcClient.test.ts
  • app/src/services/coreRpcClient.ts
  • app/src/store/coreModeSlice.test.ts
  • app/src/store/coreModeSlice.ts
  • app/src/utils/__tests__/configPersistence.test.ts
  • app/src/utils/configPersistence.ts

Comment thread app/src/utils/configPersistence.ts Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 22, 2026
…-cloudflare-2467

# Conflicts:
#	app/src/lib/i18n/chunks/de-5.ts
Copy link
Copy Markdown
Contributor

@M3gA-Mind M3gA-Mind left a comment

Choose a reason for hiding this comment

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

URL normalization for Cloudflare Tunnel self-hosted cores

Solid fix. The root cause — users pasting the tunnel base URL instead of the /rpc endpoint — is correctly addressed, and normalization is applied at every relevant entry point: picker validation, test-connection probe, getCoreRpcUrl, storeRpcUrl/getStoredRpcUrl/peekStoredRpcUrl, and coreModeSlice localStorage restoration. The CodeRabbit query/hash edge case is handled and tested.

Two minor things worth a look:

Issue–PR alignment

Issue #2467 describes a 405. The reporter's working hypothesis was "GET sent to /rpc", but the actual bug is a POST sent to / (the root endpoint). The fix is correct; the issue diagnosis was slightly off. Not blocking, just noting in case someone searches the issue later and is confused by the mismatch.

normalizeRpcUrl discards the parsed URL object

The function calls new URL(trimmed) only to validate, then re-parses the raw string by hand. The manual logic is correct for standard RPC URLs, but it diverges from the URL spec in one observable way: hostname casing is NOT normalized. new URL('HTTP://EXAMPLE.COM/') lowercases the host; the manual path returns HTTP://EXAMPLE.COM/rpc. Not a real-world issue for RPC URLs, but using the parsed object directly would eliminate this class of drift:

export function normalizeRpcUrl(url: string): string {
  const trimmed = url.trim();
  let parsed: URL;
  try { parsed = new URL(trimmed); } catch { return trimmed.replace(/\/+$/, ''); }
  // Remove trailing slashes from pathname only; append /rpc when path is root.
  parsed.pathname = parsed.pathname.replace(/\/+$/, '') || '/rpc';
  return parsed.href;
}

URL#href preserves search and hash natively, which makes firstUrlSuffixIndex unnecessary. Something to consider for a follow-up if the manual approach ever causes trouble.

Test mock diverges from real normalizeRpcUrl

See inline comment below.


Area Files Status
Core logic configPersistence.ts, coreRpcClient.ts ✓ Correct
Redux slice coreModeSlice.ts ✓ Correct
UI validation BootCheckGate.tsx ✓ Correct
New utility redactRpcUrlForLog.ts ✓ Clean
Test coverage All four touched modules have regression cases ✓ Good
CodeRabbit finding Query/hash preservation — addressed in a89c207 ✓ Addressed

describe('getCoreRpcUrl', () => {
const normalizeMockRpcUrl = (url: string) => {
const trimmed = url.replace(/\/+$/, '');
return trimmed.endsWith('/rpc') ? trimmed : `${trimmed}/rpc`;
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] normalizeMockRpcUrl is a simplified stub that doesn't match normalizeRpcUrl's behavior for URLs with query strings or leading whitespace. For example, 'https://example.com?next=/' → mock produces 'https://example.com?next=//rpc' (appends /rpc to the whole string including ?next=/), whereas the real function returns 'https://example.com/rpc?next=/'.

The test cases that use this mock all pass pre-normalized RPC URLs through, so it works today. But the test 'in web mode normalizes a stored core base URL' is checking the mock's normalization, not the real module's — if normalizeRpcUrl broke for query-bearing URLs, that test would still pass.

An easy fix: import normalizeRpcUrl from the actual module in the vi.doMock factory (using vi.importActual) rather than re-implementing it. Same pattern applies to the duplicate at line ~1085 in the getCoreRpcToken describe block.

@M3gA-Mind M3gA-Mind merged commit 3e22ffe into tinyhumansai:main May 22, 2026
30 of 47 checks passed
@M3gA-Mind
Copy link
Copy Markdown
Contributor

thanks @YUHAO-corn, the url normalization is such a quality of life fix, no more broken rpc connections when users paste a bare base url 🔥 great to see the regression coverage in there too, really solid work as always!

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.

Desktop Client returns 405 when connecting to self-hosted Core via Cloudflare Tunnel

2 participants