Stt_whisper2 on remote HaRP daemon: "heartbeat check failed" while ExApp is healthy (NC 32.0.14, app_api 32.0.0)

Hi everyone,

I’m hitting a persistent “heartbeat check failed” on stt_whisper2 2.5.0
when I deploy it on a remote HaRP daemon, on a fresh Nextcloud 32.0.14
(upgraded from 31.0.14 last week). Both the ExApp container and HaRP are
healthy, the FRP tunnel establishes cleanly on every restart, and from
inside the ExApp I can reach http://<nextcloud-lan-ip>/status.php
with niquests (three back-to-back → 200/200/200). Yet
occ app_api:app:enable returns “Failed to enable ExApp stt_whisper2”
and no provider is registered — the Assistant’s “Work with audio” tab
only shows TTS.

I’ve eliminated the usual suspects (NC 32 upgrade artifacts, the
documentserver_community boot fatal, wrong NC_INSTANCE_URL, HTTP/2
races, DSP-vs-HaRP confusion, storage-driver issues) — I’ve put all the
details below to save a round-trip.

What I’m hoping for: the exact HTTP flow that constitutes the
“heartbeat check” during an enable on a remote HaRP daemon — HaRP shows
no incoming request at that moment, which puzzles me. Any pointer on
where to tcpdump or which log/loglevel to raise would be much
appreciated. Full report follows.

Thanks a lot!

Hi @claude.dilangue

Thank you for reporting this!

So the heartbeat is an HTTP GET from the Nextcloud PHP process to Nextcloud’s own public
URL, path /exapps/stt_whisper2/heartbeat, carrying harp-shared-key, ex-app-host
(127.0.0.1), ex-app-port, EX-APP-ID, EX-APP-VERSION, AUTHORIZATION-APP-API. It is the
reverse proxy that must route /exapps/ to HaRP’s HP_EXAPPS_ADDRESS (:8780); HaRP’s SPOE
agent then rewrites the path and dials 127.0.0.1:<app_port>, which frps maps through the
FRP tunnel to /tmp/exapp.sock in the container. It retries for 10 minutes (60 * 10
attempts, 1s apart) before giving up.

Why every symptom fits a missing /exapps/ proxy rule

  • The FRP tunnel (:8782) and ExApp→Nextcloud /status.php both exercise the outbound
    direction. The heartbeat is the inbound direction — completely different path.
  • The container healthcheck is meaningless here: healthcheck.sh in this repo only does
    pgrep -x frpc. “Healthy” says nothing about whether uvicorn is serving /heartbeat.
  • No provider registered → correct, providers are registered in enabled_handler, which
    only runs on the PUT /enabled after a successful heartbeat.
  • If nextcloud_url is a LAN IP (which your http:///status.php test
    suggests), the heartbeat goes to http:///exapps/… — and the LAN vhost almost
    certainly has no /exapps/ block even when the public domain does. Nextcloud answers
    404 itself and HaRP never sees anything. HaRP’s own source even calls this out: “a
    misconfiguration on that trusted path (e.g. reverse-proxy stripping /exapps/)”
    (haproxy_agent.py:385)

You can try the following diagnostics:

1. occ app_api:daemon:list — read the HaRP daemon’s nextcloud_url.
2. From the Nextcloud host (inside the php container if containerized):
curl -i <nextcloud_url>/exapps/stt_whisper2/heartbeat

  • 401 Unauthorized as content-type: text/plain → HaRP was reached, routing is fine,
    look at auth/SPOE.
  • Nextcloud’s HTML/JSON 404 → root cause found: reverse proxy has no /exapps/ rule.
  • Connection refused/timeout → Nextcloud can’t reach its own URL (split-horizon DNS /
    hairpin NAT).
  1. Set loglevel to 0/1 and grep nextcloud.log for Performing heartbeat on: — it logs the
    exact URL, which answers their question directly.
  2. Restart HaRP with HP_LOG_LEVEL=info; tcpdump -i any -n port 8780 on the HaRP host.

The fix is most likely the location /exapps/ { proxy_pass http://:8780/exapps/; } block
(per the HaRP README) on whichever vhost nextcloud_url points at, with
proxy_read_timeout 1800s

I hope this helps!

The distinction between an ExApp container health check and the PHP process heartbeat is useful here. I would compare the reverse-proxy access log with the Nextcloud log while running the failing command: the request should appear on the public/external route, not only on the FRP tunnel. Checking the response status and content type is especially helpful, because a plain-text 401 means the request reached HaRP while an HTML 404 usually means the proxy selected the wrong location. I would also verify that the daemon’s configured Nextcloud hostname resolves to the same address from the Nextcloud host/container; split DNS can make a browser test look fine while the PHP-side request takes a different path.