Skip to content

[BUG] Self-signed / private-CA WebSocket connections fail — "Trust self-signed certificate" toggle only applies to HTTP, and user-installed CAs are not trusted natively[BUG] #469

@neoge

Description

@neoge

App Version

2.6.8 (109)

Platform

  • Android
  • iOS

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:

  1. A CA installed by the user via Android settings is trusted natively for both HTTP and WSS, without any "Trust self-signed certificate" workaround.
  2. When the toggle is enabled, it actually applies to WebSocket as well, not only to HTTP.

How to reproduce

  1. Put an OpenWebUI instance behind nginx (or any TLS terminator) with a certificate signed by a private/internal CA.
  2. Install that CA on the Android device via Settings → Security → Install certificate → CA certificate.
  3. 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.
  4. Sign in. Confirm that models, chats, settings load fine (HTTP works).
  5. Open any chat and send a message.
  6. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    Status

    In review

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions