Skip to content

[Bug]: Network failures are all reported as "Failed to connect" — TLS/certificate errors are invisible #1954

Description

@MikeBishop

I have verified that the bug is not already awaiting release

Yes

What was the Problem?

ApiHandler.makeRequest collapses every IOException into "Failed to connect", logged at Log.d with printStackTrace(), never through AbsLogger. So:

  • A TLS trust failure looks identical to an unreachable server.
  • The exception never reaches Settings → Logs.
  • The only way to see the real cause is adb logcat on System.err.

ApiHandler.kt#L113-L120:

override fun onFailure(call: Call, e: IOException) {
  Log.d(tag, "FAILURE TO CONNECT")
  e.printStackTrace()

  val jsobj = JSObject()
  jsobj.put("error", "Failed to connect")
  cb(jsobj)
}
  • Log.d — debug level, commonly filtered out.
  • printStackTrace() — goes to System.err, not AbsLogger, so it never reaches the in-app log viewer. onResponse right below this uses AbsLogger.info for the 401 case, so the omission looks like an oversight rather than a choice.
  • "Failed to connect" — discards the exception type and message before the UI ever sees them.

Any ApiHandler call — playback, download, sync — reports a bad cert the same as an unreachable server. That's common and reproduces trivially.

A narrower, costlier case: login/browse (CapacitorHttp) succeeds or fails informatively against a server, while ApiHandler fails opaquely against what looks like the same server, because the two paths actually resolved to different endpoints (split-horizon DNS, path-based reverse proxy, or the NAT64/DNAT case in #1901). Not everyday, but it's what turns a five-minute check into a long investigation, since nothing distinguishes "wrong cert" from "no route" until you pull logcat.

Steps to Reproduce the Issue

  1. Put an untrusted cert in front of an ABS server — e.g. openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout key.pem -out cert.pem -subj "/CN=abs-test.local", not installed on the test device. (A DNAT misconfiguration like [Bug]: Certain app functions fail over IPv6 #1901 produces the same failure; a self-signed cert is just easier to set up.)
  2. Add the server and log in fresh (Settings → Server → Add Server) — goes through CapacitorHttp, not ApiHandler.
    • handleLoginFormError surfaces something reasonably specific here: "Failed to contact server. (${error})". Not the code path this issue is about, but it shows the app is capable of a real message.
  3. With the server added, trigger playback, a download, or progress sync — all go through ApiHandler.makeRequest (OkHttp).
    • All three fail with the same bare "Failed to connect", no matter the actual cause. Settings → Logs shows nothing.
  4. adb logcat -v time System.err:V *:S shows what the app doesn't:
W/System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
	at okhttp3.internal.connection.RealConnection.connectTls(RealConnection.kt:379)
	at okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.kt:337)
	at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:209)
	...
D/ApiHandler: FAILURE TO CONNECT
E/ApiHandler: Failed to connect

The app can report a real error — step 2 proves that. ApiHandler.makeRequest just doesn't.

What was Expected?

A certificate/TLS failure should be reported as one, not folded into a generic connectivity error, in both the UI and the in-app logs.

Phone Model

OnePlus 11

Phone OS

Android 16

Audiobookshelf App Version

Android App - 0.13.0

Installation Source

Google Play Store

Additional Notes

Why this matters

I filed #1901 as an IPv6 bug. The real cause was a misscoped DNAT rule on my firewall: NAT64 traffic wasn't being translated, so the phone was hitting the firewall's own GUI listener and its self-signed bootstrap cert instead of the real server.

The app was right to refuse that cert. But the only signal was Failed to connect, so the misdiagnosis stood for seven weeks.

Confirmed with curl on the device — same hostname, two different certs depending on which address gets used:

$ adb shell curl -kv https://audiobooks.home.bishop.be
*   Trying [fd67:9255:a0e:e::100]:443...
*  subject: CN=*.bishop.be
*  issuer: C=US; O=Let's Encrypt; CN=YR2

$ adb shell curl -4kv https://audiobooks.home.bishop.be
*   Trying 192.168.150.1:443...
*  subject: ... O=OPNsense bootstrap web certificate; CN=opnsense-primary.home.bishop.be
*  issuer: ... O=OPNsense bootstrap web certificate; CN=opnsense-primary.home.bishop.be

"certificate trust failure: CN=opnsense-primary.home.bishop.be" would have gotten me to the firewall rule in about ninety seconds.

Related

InternalDownloadManager.kt#L42-L45 has a milder version — logs at Log.e with the exception attached, better, but still bypasses AbsLogger and reports upstream as a bare onComplete(true):

override fun onFailure(call: Call, e: IOException) {
  Log.e(tag, "Download URL failed", e)
  progressCallback.onComplete(true)
}

A failed download gives the user no indication why.

AI involvement

Claude was used to explore the app's source, form and discard several hypotheses (Happy Eyeballs, an Android DNS-resolution bug, sequential-connect timeouts, CLAT/RFC 6724 address selection), and draft this writeup. I directed the investigation, ran all adb/openssl commands against my own devices and network, and confirmed the actual root cause (the DNAT scope on my firewall, and the two differing certificates) myself before accepting it. The suggested code change is untested — I haven't built or run it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions