App Version
2.6.8 (109)
Platform
How you installed it
Google Play
What happened?
When connecting to an OpenWebUI instance behind nginx with a TLS certificate signed by a private/internal CA, all HTTP API calls work after enabling "Trust self-signed certificate" in the advanced settings — but the Socket.IO/WebSocket channel never establishes. As a result, chat streaming hangs: the server processes the message, generates the answer, and pushes tokens to the (non-existent) Socket.IO session, while the POST /api/chat/completions body never receives the stream. Conduit eventually shows "An unexpected error occurred while processing your request."
Server-side proof that the problem is not the proxy or backend:
- nginx access log shows zero requests to
/ws/socket.io/ from Conduit, ever — not even a TLS handshake. The same proxy with the same cert returns 101 Switching Protocols for a plain curl -k --upgrade websocket request, so server-side WS is healthy.
- Client-side packet capture shows short TLS sessions opening/closing on port 443 with no application-layer data — consistent with cert validation failing client-side, before any HTTP/WS upgrade is sent.
- Replaying the exact
POST /api/chat/completions body that Conduit sends, directly against the backend, returns either null after generation completes, or hangs — depending on whether chat_id/background_tasks are present in the body. With those fields, OpenWebUI assumes the client has a Socket.IO listener; without one, the HTTP body becomes useless.
Tested on Android 16.
What should have happened?
The WebSocket transport should use the same TLS trust configuration as the HTTP transport — so that:
- A CA installed by the user via Android settings is trusted natively for both HTTP and WSS, without any "Trust self-signed certificate" workaround.
- When the toggle is enabled, it actually applies to WebSocket as well, not only to HTTP.
How to reproduce
- Put an OpenWebUI instance behind nginx (or any TLS terminator) with a certificate signed by a private/internal CA.
- Install that CA on the Android device via Settings → Security → Install certificate → CA certificate.
- Open Conduit, enter
https://<host>/, and enable "Trust self-signed certificate" in the advanced settings under the URL field — otherwise even the initial connection fails despite the CA being installed.
- Sign in. Confirm that models, chats, settings load fine (HTTP works).
- Open any chat and send a message.
- Observe the streaming reply never arrives. After ~30 s, an error banner appears.
Does this happen every time?
Yes, every time
When did this start?
Started after putting OpenWebUI behind nginx with a private-CA TLS certificate. With Conduit pointing directly at unencrypted http://host:8080/ it worked.
Screenshots
No response
Logs
Symptom on the server side
192.168.x.y - - [09/May/2026:02:15:53 +0200] "POST /api/chat/completions HTTP/1.1" 499 0 rt=30.056 uct="0.000" uht="-" urt="30.057" "Dart/3.11 (dart:io)"
uht="-" = upstream sent zero response headers; client closed after 30 s. The same backend, when called with Accept: text/event-stream and without chat_id, streams perfectly. So the backend is fine — it's just expecting Socket.IO listeners that Conduit never creates because WSS dies in the TLS handshake.
Root cause (from reading the source)
I cloned the repo and traced the two issues:
1. android/app/src/main/AndroidManifest.xml has no android:networkSecurityConfig.
On Android 7+, apps only honor user-installed CAs if a network_security_config.xml declares <certificates src="user"/>. Without it, neither HTTP nor WS trusts user-installed CAs by default, and the toggle becomes the only escape hatch.
2. lib/core/services/socket_tls_override_impl_io.dart lines 21–25 don't actually apply the override to the WS handshake:
return HttpOverrides.runWithHttpOverrides<io.Socket>(
() => io.io(base, builder.build()),
_ScopedServerTlsOverrides(serverConfig),
);
io.io(base, builder.build()) only constructs the socket synchronously. The actual TLS handshake performed by the WebSocket transport happens asynchronously on the OS event loop, after runWithHttpOverrides has returned and the zone-scoped override is gone. WebSocket.connect() therefore falls back to the default SecurityContext, the cert is rejected, and the connection dies before it ever reaches the server — matching the "zero /ws/socket.io/ hits in the proxy log" observation exactly.
The HTTP path is not affected because ServerTlsHttpClientFactory.configureDio (server_tls_http_client_factory.dart:39) installs the custom HttpClient directly on the Dio adapter — no zone scoping involved.
Suggested fixes
Fix 1 — android/app/src/main/res/xml/network_security_config.xml (preferred):
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/>
</trust-anchors>
</base-config>
</network-security-config>
Reference it from AndroidManifest.xml via android:networkSecurityConfig="@xml/network_security_config". This fixes the symptom for users who properly install the CA on the device, without any toggle.
Fix 2 — make the toggle actually work for WS (independently necessary):
Replace the zone-scoped runWithHttpOverrides with a global override held for the lifetime of the SocketService, e.g. set HttpOverrides.global = _ScopedServerTlsOverrides(serverConfig) when the service starts and restore the previous global on dispose. Alternatively, when allowSelfSignedCertificates is true, force transports: ['polling'] — Dio's HTTP path already handles cert overrides correctly, and polling will work without a WS-level fix at the cost of higher latency.
App Version
2.6.8 (109)
Platform
How you installed it
Google Play
What happened?
When connecting to an OpenWebUI instance behind nginx with a TLS certificate signed by a private/internal CA, all HTTP API calls work after enabling "Trust self-signed certificate" in the advanced settings — but the Socket.IO/WebSocket channel never establishes. As a result, chat streaming hangs: the server processes the message, generates the answer, and pushes tokens to the (non-existent) Socket.IO session, while the
POST /api/chat/completionsbody never receives the stream. Conduit eventually shows "An unexpected error occurred while processing your request."Server-side proof that the problem is not the proxy or backend:
/ws/socket.io/from Conduit, ever — not even a TLS handshake. The same proxy with the same cert returns101 Switching Protocolsfor a plaincurl -k --upgrade websocketrequest, so server-side WS is healthy.POST /api/chat/completionsbody that Conduit sends, directly against the backend, returns eithernullafter generation completes, or hangs — depending on whetherchat_id/background_tasksare present in the body. With those fields, OpenWebUI assumes the client has a Socket.IO listener; without one, the HTTP body becomes useless.Tested on Android 16.
What should have happened?
The WebSocket transport should use the same TLS trust configuration as the HTTP transport — so that:
How to reproduce
https://<host>/, and enable "Trust self-signed certificate" in the advanced settings under the URL field — otherwise even the initial connection fails despite the CA being installed.Does this happen every time?
Yes, every time
When did this start?
Started after putting OpenWebUI behind nginx with a private-CA TLS certificate. With Conduit pointing directly at unencrypted
http://host:8080/it worked.Screenshots
No response
Logs
Symptom on the server side
uht="-"= upstream sent zero response headers; client closed after 30 s. The same backend, when called withAccept: text/event-streamand withoutchat_id, streams perfectly. So the backend is fine — it's just expecting Socket.IO listeners that Conduit never creates because WSS dies in the TLS handshake.Root cause (from reading the source)
I cloned the repo and traced the two issues:
1.
android/app/src/main/AndroidManifest.xmlhas noandroid:networkSecurityConfig.On Android 7+, apps only honor user-installed CAs if a
network_security_config.xmldeclares<certificates src="user"/>. Without it, neither HTTP nor WS trusts user-installed CAs by default, and the toggle becomes the only escape hatch.2.
lib/core/services/socket_tls_override_impl_io.dartlines 21–25 don't actually apply the override to the WS handshake:io.io(base, builder.build())only constructs the socket synchronously. The actual TLS handshake performed by the WebSocket transport happens asynchronously on the OS event loop, afterrunWithHttpOverrideshas returned and the zone-scoped override is gone.WebSocket.connect()therefore falls back to the defaultSecurityContext, the cert is rejected, and the connection dies before it ever reaches the server — matching the "zero/ws/socket.io/hits in the proxy log" observation exactly.The HTTP path is not affected because
ServerTlsHttpClientFactory.configureDio(server_tls_http_client_factory.dart:39) installs the customHttpClientdirectly on the Dio adapter — no zone scoping involved.Suggested fixes
Fix 1 —
android/app/src/main/res/xml/network_security_config.xml(preferred):Reference it from
AndroidManifest.xmlviaandroid:networkSecurityConfig="@xml/network_security_config". This fixes the symptom for users who properly install the CA on the device, without any toggle.Fix 2 — make the toggle actually work for WS (independently necessary):
Replace the zone-scoped
runWithHttpOverrideswith a global override held for the lifetime of the SocketService, e.g. setHttpOverrides.global = _ScopedServerTlsOverrides(serverConfig)when the service starts and restore the previous global on dispose. Alternatively, whenallowSelfSignedCertificatesis true, forcetransports: ['polling']— Dio's HTTP path already handles cert overrides correctly, and polling will work without a WS-level fix at the cost of higher latency.