Fix Clash Dashboard Not Loading: external-controller & external-ui Checklist Steps

What This Guide Addresses

You already ticked every box inside the tray app description: external controller enabled, maybe a bundled Yacd or Metacubexd button, REST port remembered as 9090. Then you open your browser at localhost and hear silence—connection refused, a forever spinner, or a blank white rectangle where the SPA should hydrate. Meanwhile Clash itself proxies ordinary sites just fine because normal traffic never touches the Mihomo REST surface. Misery loves company: this symptom is one of the most common help-channel topics, yet the fix rarely lives inside subscription URLs or GEOIP tweaks. Instead you need disciplined inspection of bind addresses, YAML field placement, mixin merge precedence, Mixed Content borders between HTTPS dashboards and plaintext APIs, accidental double definitions of external-controller, and—only after the socket truly listens—the usual firewall chatter.

This article stands apart from sprawling tutorials on global mode, tun stacks, sniffers, or rule providers. Those documents matter elsewhere; here only the diagnostic slice around external-controller, optional external-ui, and localhost ergonomics earns space. Where another piece already dives deep—for example resolving “port already in use” collisions—we point there instead of pasting duplicated steps.

Mental Model: Three Separate Layers

Separate three ideas before you poke YAML. Layer one is listening: Mihomo binds a TCP port for its REST shim. Layer two is serving HTML: embedded static UI bundles (when external-ui resolves) or detached front ends you host yourself—both ultimately call the same REST port for JSON. Layer three is trust: secrets, bearer headers, Mixed Content blocking, LAN exposure, accidental exposure of plaintext tokens to foreign Wi-Fi—all security topics sitting above raw connectivity. Symptoms can fail at layer one (127.0.0.1 rejects because nobody listens yet) while subscription sync still works; never assume “internet works ⇒ dashboard works.” Conversely, fixing layer one does nothing if Mixed Content shackles scripts at layer three.

Step 1 — Confirm the REST Endpoint With curl

Forget the flashy UI for ninety seconds and ask the simplest question on earth: does http://CONTROLLER/version answer? Suppose your eventual goal is controller at loopback port 9090. From the same OS user session that launches the tray app, execute:

curl -sS http://127.0.0.1:9090/version

If you did not enable TLS or custom API base paths, plaintext HTTP suffices for the first probe. Immediate refusal strongly suggests Mihomo either failed to bind, bound another port, snapped to IPv6-only [::1], or your curl command targets wrong digits. Immediate JSON output means REST is healthy—you will later chase browser SPA issues separately. Curl errors referencing connection reset versus timeout still differ: resets often mean RST from firewall policy; timeouts mean routing black holes or captive intermediaries rewriting traffic—rare strictly for loopback, but imaginable across containers.

When curl fails, open the Mihomo runtime log—not the glossy GUI façade—and read the earliest lines mentioning external-controller or RESTful API. Modern cores print the finalized listen directive after mixin merges. Capture that exact host:port string; treat it as your single ground truth regardless of fuzzy memory.

Step 2 — external-controller Grammar and Placement

YAML tolerates indentation, yet humans stumble more than parsers. Typical minimal keys sit at profile root—not nested unintentionally beneath tun: or dns: unless you knowingly extend provider-specific overlays. Canonical shape resembles:

external-controller: 127.0.0.1:9090
secret: "replace-me"

Some templates abbreviate:

external-controller: :9090

That shorthand listens on every interface—functionally akin to binding 0.0.0.0 for IPv4 plus visible dual-stack combos depending on implementation. Convenience trades away safety: every device on reachable networks may attempt hits unless firewalls constrain. Narrowing voluntarily to loopback minimizes accidental exposure behind coffee-shop Wi-Fi. If shorthand caused confusion (“which address should I bookmark?”), revert to explicit 127.0.0.1:9090 until understanding returns.

Multiple definitions of external-controller silently fight: later merges win or silently drop depending on tool-specific patch behavior. Inspect final effective config produced by GUIs—they often concatenate base profile + mixin + remote patch. Surprise removal after remote sync commonly explains regressions occurring “Friday only.” Snapshot your effective rendered profile after each remote pull when debugging ghost REST outages.

Step 3 — external-ui Versus Third-Party Hosting

external-ui points Mihomo toward a filesystem directory hosting static SPA assets packaged with your distribution—or downloaded separately for air-gapped machines. Typical relative paths appear small—just a handful of ASCII characters—but they must correctly resolve from the Mihomo binary working directory, not whichever folder your editor last opened.

When you consciously host Yacd or Metacubexd on a standalone static HTTP server—even local Python http.server—you still instruct the SPA where the API lives via environment parameters or bundled config fields. Serving UI at HTTPS while the API listens plain HTTP on localhost explodes Mixed Content shields; we dissect that mismatch next. Embedding via /ui route from Mihomo avoids cross-origin juggling when both originate same scheme and host—but not every GUI exposes that route cleanly. Document whichever combination you stabilized so future reinstalls mimic it verbatim.

Step 4 — Mixed Content: HTTPS Panels Calling HTTP localhost

Browsers escalate Mixed Content ruthlessly modern versions. Symptoms look like stalled network rows inside Developer Tools labelled blocked:mixed-content. Common pattern: bookmarking official public Yacd hosting over HTTPS (https://yacd.haishan.me style deployments) yet pointing secrets at http://127.0.0.1:9090. The SPA loads over HTTPS, then JavaScript forbids XMLHttpRequest downgrade to plaintext unless explicitly exempted—which typical corporate policies forbid.

Workable exits include: switching to an HTTP-hosted panel copy on localhost (same scheme), tunneling localhost through HTTPS via reverse proxy tricks—overkill domestically—or using native embedded UI fetched over HTTP from Mihomo (http://127.0.0.1:9090/ui). Another pragmatic approach: temporarily open Mihomo-managed controller over HTTPS experimental flags—few maintainers advertise stable ergonomics though. When diagnosis matters more than glossy polish, run two browser profiles: Chromium with temporarily relaxed mixed-content instrumentation is dangerous; prefer structural alignment rather than weakening global security sliders.

Step 5 — IPv4 localhost Versus IPv6 (::1)

You typed http://localhost:9090 believing universality yet the OS resolver maps localhost to ::1 first while Mihomo listens only IPv4-specific 127.0.0.1. Connection refused echoes ensue—not because controllers died, merely address family divergence. Narrow fix: always curl and browse explicit 127.0.0.1 during setup, or widen bind family with dual-stack listening if Mihomo exposes that toggle. Conversely if you bind [::1]:9090, IPv4 curls fail until matched. Treat address families as picky puzzle pieces snapping only when aligned symmetrically.

Step 6 — When the Controller Port Matches Another Daemon

Symptoms overlap brute-force refusal: Mihomo refuses start or quietly suppresses REST while leaving proxy listeners alive when partial startup paths exist—but often the entire tray shows red. Before spiraling imaginative firewall fantasies, confirm another process latched TCP 9090. Our dedicated Windows-centric walkthrough Fix Clash Port in Use on Windows explains netstat, PID mapping, and YAML changes without repeating LAN policy nuance unrelated here. Cross-platform analogues behave similarly:

sudo lsof -iTCP:9090 -sTCP:LISTEN on macOS or Linux.

Free the port by retiring the offending service or remap external-controller. Update every downstream consumer—including saved browser bookmarks—to the new numbering.

Step 7 — secret, Bearer Headers, and “Unauthorized” AJAX

Setting non-empty secrets secures unattended REST exposures on permissive binds. SPA panels transmit secrets either via POST body handshake or Bearer headers depending on tooling generation. Unauthorized JSON responses confuse beginners who wrongly assume transports failed—it was HTTP 401 semantics. Browser devtools differentiate quickly: handshake requests might preflight OPTIONS when stray CORS interplay surfaces; loopback setups rarely escalate there but remote hosted panels injecting cross-origin XHR occasionally do.

When iterating secrets, recycle both YAML and UI fields simultaneously; mismatched remnants produce silent failure modes worse than blatant resets. Clearing site storage for SPA domains removes stale bearer tokens lurking from earlier experiments.

Step 8 — LAN Access Versus Secure Defaults

Phones or tablets probing your laptop REST surface require explicit bind allowances plus OS firewall ingress rules. Narrow discussion appears in Allow LAN Mobile Proxy Firewall guidance; we avoid duplicating exhaustive phone-side toggles beyond noting controller exposure shares risk classes with SOCKS listeners. Principle: widen bind only consciously, constrain by secret or reverse proxy overlays, revisit after roaming networks change.

Step 9 — Docker Desktop and Published Ports

Containerized Mihomo listens inside network namespaces. Binding external-controller to bare 127.0.0.1 inside container isolates REST away from macOS host loopback—even if port-publish flags exist—because namespaces diverge philosophically unless you map host-gateway bridging patterns. Reliable recipe: expose 0.0.0.0:9090 inside container, docker publish -p 9090:9090, browse host-side 127.0.0.1:9090.

WSL2 hybrids introduce another shim: localhost forwarding sometimes lags distributions updates; curling from Windows CMD versus Linux shell hits distinct stacks. Decide canonical debugging shell first; align bookmarks accordingly rather than bouncing randomly.

Step 10 — GUI Overrides That Erase YAML Edits

Powerful forks persist partial settings inside opaque databases syncing back over hand-edited YAML. You append external-ui manually; next launch duplicates vanish silently because GUI rewrote authoritative store. Symptoms feel paranormal until you diff effective output after restarts twice. Harmonize workflows: mutate through UI fields when available, exporting final YAML as canonical truth for version control—not half-stale fragments.

Step 11 — End-to-End Verification Checklist

Walk top to bottom calmly once:

R1: Core log declares controller listening precisely where you anticipate.

R2: curl returns JSON baseline without certificate warnings or HTTP error codes signaling auth failures.

R3: Browser uses matching IP family targeting loopback—not ambiguous localhost while debugging bifurcation.

R4: SPA panel scheme aligns API scheme expectations—Mixed Content consoles stay clean.

R5: After changing binds, revisit bookmarked launcher URLs—not cached older ports cached inside OS autocomplete.

Safety reminder. Never expose unrestricted REST binds to hostile networks absent secrets layered with firewall discipline. Convenience debugging on hotel Wi-Fi differs dramatically from responsibly locking down unattended laptop sleep states.

Closing Outlook

Dashboard dramas rarely indict upstream kernel quality—they expose configuration drift and misunderstandings about layering, stacked atop otherwise healthy cores. Teach yourself to treat REST reachability independently from SOCKS success: each listener carries unique bind semantics, merges, firewall placement, Mixed Content interplay, occasional Docker namespace barriers. Once distilled, most incidents compress into repeatable minutes rather than frantic reinstall loops.

After structure returns, consolidating installation sources matters: grab updated clients from a single authoritative entry so version skew does not resurrect stale defaults when you reinstall next quarter. Ready for onboarding tasks like subscription imports afterward? Explore how to import Clash subscription links everywhere once your panel stays steady. Until then—→ Download Clash for free and experience the difference.

Still wrestling bind contention after curl proves nothing listens? Pivot to Windows port-resolution steps or revisit LAN exposure patterns in our Allow LAN walkthrough. Go to the download page →