On Android, a request to an unreachable Ollama server surfaces as:
Ninjii error: failed to connect to /192.168.50.11 (port 11434) ...
The leading / is java.net.InetSocketAddress.toString(), which formats as hostname/literal-address; connecting to a raw IP leaves the hostname half empty. Capacitor uses CapacitorHttpUrlConnection (plain HttpURLConnection, no OkHttp), so this is Android's libcore wording reaching the user verbatim.
resolveQueryError (app/src/lib/query/query-error.ts) special-cases only 401; everything else falls through to t(fallbackKey, { error: message }) with the raw .message interpolated. assistant.error_generic is "Ninjii error: {{error}}", so whatever the platform networking layer says goes straight to the user.
Consequences:
- Untranslatable.
{{error}} is always English platform text, in all five locales.
- Platform-dependent. Electron says
connect ECONNREFUSED 192.168.50.11:11434, iOS URLSession says Could not connect to the server, and browser fetch says Failed to fetch with no address at all, so web users cannot see which host failed.
- No guidance. Nothing tells the user to check the address or start the server.
This affects every caller of resolveQueryError, not just the assistant: an unreachable ZoneMinder server shows the same string in its error banner.
Fix: carry the address from the request, do not parse it out of the message
lib/http.ts's catch already has fullUrl in scope and rethrows untouched. Stamp the host there, so it is identical on every adapter:
if (httpError.host === undefined) {
try { httpError.host = new URL(fullUrl).host; } catch { /* relative URL */ }
}
new URL(fullUrl).host gives 192.168.50.11:11434: host and port, no scheme, no path, no stray /.
Detect unreachability structurally rather than by string match: a connection failure never received a response, so it has no status, while an HTTP error does. No regex over four platforms' wordings, which would drift.
Aborts must be excluded so a user cancellation does not render as a connection failure. Note isTimeoutError folds aborts in and is therefore the wrong helper here; isAbortError is the right one.
Result is one localized string with a single variable, which does translate:
Can't reach 192.168.50.11:11434. Check the address in Settings, or that the server is running.
The address stays visible, which is wanted, but comes from the request rather than from platform prose. It also appears on web and iOS, where the platform message currently hides it.
On Android, a request to an unreachable Ollama server surfaces as:
The leading
/isjava.net.InetSocketAddress.toString(), which formats ashostname/literal-address; connecting to a raw IP leaves the hostname half empty. Capacitor usesCapacitorHttpUrlConnection(plainHttpURLConnection, no OkHttp), so this is Android's libcore wording reaching the user verbatim.resolveQueryError(app/src/lib/query/query-error.ts) special-cases only 401; everything else falls through tot(fallbackKey, { error: message })with the raw.messageinterpolated.assistant.error_genericis"Ninjii error: {{error}}", so whatever the platform networking layer says goes straight to the user.Consequences:
{{error}}is always English platform text, in all five locales.connect ECONNREFUSED 192.168.50.11:11434, iOSURLSessionsaysCould not connect to the server, and browserfetchsaysFailed to fetchwith no address at all, so web users cannot see which host failed.This affects every caller of
resolveQueryError, not just the assistant: an unreachable ZoneMinder server shows the same string in its error banner.Fix: carry the address from the request, do not parse it out of the message
lib/http.ts's catch already hasfullUrlin scope and rethrows untouched. Stamp the host there, so it is identical on every adapter:new URL(fullUrl).hostgives192.168.50.11:11434: host and port, no scheme, no path, no stray/.Detect unreachability structurally rather than by string match: a connection failure never received a response, so it has no
status, while an HTTP error does. No regex over four platforms' wordings, which would drift.Aborts must be excluded so a user cancellation does not render as a connection failure. Note
isTimeoutErrorfolds aborts in and is therefore the wrong helper here;isAbortErroris the right one.Result is one localized string with a single variable, which does translate:
The address stays visible, which is wanted, but comes from the request rather than from platform prose. It also appears on web and iOS, where the platform message currently hides it.