Summary
A Flutter application failed to establish a WebSocket connection to an ESP32 acting as a Wi-Fi Access Point (AP). Despite being able to ping the target IP (192.168.1.1) and having configured usesCleartextTraffic="true", the connection resulted in a WebSocketChannelException or Connection Refused. The issue stems from a mismatch between Layer 3 connectivity (ICMP/Ping) and Layer 4/7 application constraints (TCP/HTTP/WebSocket) within the Android networking stack when interacting with non-internet-connected local gateways.
Root Cause
The failure is caused by a combination of three specific technical factors:
- Android Captive Portal Detection: When an Android device connects to an AP that lacks internet access (like an ESP32), the OS attempts to reach a “connectivity check” URL. If it fails, the OS flags the network as “No Internet” and may restrict background or even foreground socket traffic to prevent data leakage or due to “Smart Network Switch” logic.
- Port Zero / URI Misconfiguration: The error
uri = http://192.168.1.1:0indicates that the connection attempt is being initialized with an invalid port (0). In many socket implementations, port 0 is a request for the OS to assign an ephemeral port, which is invalid for a client attempting to connect to a specific service. - TCP Handshake vs. ICMP: A successful Ping only confirms that the ESP32’s network stack is alive (ICMP). It does not guarantee that the TCP stack is accepting connections on port 80 or that the WebSocket upgrade handshake can complete.
Why This Happens in Real Systems
In production IoT environments, this happens because:
- Network State Assumptions: Developers often assume that if the device is “connected to Wi-Fi,” the network is fully operational. However, Android and iOS treat “No Internet” networks differently than standard LANs.
- OS-Level Interception: Modern mobile OSs implement aggressive Network Security Configurations. Even if
usesCleartextTrafficis true, the OS may intercept requests to local IPs that it deems “untrusted” or “unreachable” via standard DNS/Gateway routes. - Socket Lifecycle: WebSocket connections require a successful HTTP Upgrade handshake. If the underlying TCP connection is reset by the OS (due to perceived network instability), the client receives a
Connection closed before full header was receivederror.
Real-World Impact
- Device Brick-like Behavior: Users perceive the hardware as “broken” because the app fails to connect, even though the hardware is functioning perfectly.
- Intermittent Connectivity: In industrial settings, as devices move between different APs, the app may work on one network but fail on another purely due to OS routing policies.
- Development Friction: Highly skilled engineers can spend days debugging firmware (ESP32) when the issue is actually an Android OS networking policy.
Example or Code
The primary error in the provided snippet is the potential for a malformed URI and the failure to handle the lifecycle of the socket.
import 'package:web_socket_channel/web_socket_channel.dart';
// FIX: Ensure the port is explicitly defined and the URI is correctly formatted.
// Avoid port 0; explicitly use 80 or the specific port defined in ESP32 firmware.
void connectToEsp32() {
try {
final uri = Uri.parse("ws://192.168.1.1:80");
final channel = WebSocketChannel.connect(uri);
channel.stream.listen(
(message) => print("Received: $message"),
onError: (error) => print("Socket Error: $error"),
onDone: () => print("Connection Closed"),
);
} catch (e) {
print("Connection Initialization Failed: $e");
}
}
How Senior Engineers Fix It
- Validate Port Specification: Ensure the URI does not resolve to port 0. In Flutter/Dart, always explicitly define the port in the
Uriobject. - Bypass Captive Portal Logic: For ESP32 AP mode, the engineer should configure the ESP32 to serve a simple Captive Portal (DNS hijacking) so the Android device believes it has successfully “logged in” to the network, preventing the OS from restricting traffic.
- Layered Testing: Instead of jumping to WebSockets, first test connectivity using a raw TCP Socket check to confirm the port is open, then an HTTP GET to confirm the server responds, before attempting the complex WebSocket handshake.
- Explicit Network Configuration: Utilize
NetworkSecurityConfigto explicitly whitelist the local IP range, ensuring the OS doesn’t block the “insecure” local traffic.
Why Juniors Miss It
- Confusing Ping with Connectivity: Juniors often see a successful
pingand assume the Application Layer is also reachable. They miss the distinction between ICMP and TCP. - Over-reliance on Manifest Permissions: Juniors often think adding
INTERNETpermission solves all problems, failing to realize that OS-level network intelligence (Captive Portal detection) can override application permissions. - Ignoring the URI: They often overlook subtle errors in URI construction, such as accidental port 0 assignments or incorrect protocol prefixes (
ws://vshttp://).