requests: guard against getaddrinfo() hang when WiFi not connected
Summary
The request() function in requests/__init__.py calls socket.getaddrinfo() (line 81) without verifying network connectivity. On platforms where getaddrinfo() blocks indefinitely when the WiFi interface is active but not connected (see micropython/micropython#18797), this causes the device to freeze with no way to recover except a hard reset.
The timeout parameter passed to requests.get() has no effect because socket.settimeout() is applied to the socket object (line 93) — which is created after getaddrinfo() returns. So the hang occurs before any timeout takes effect.
Suggested fix
Add a connectivity check before getaddrinfo():
# Guard: getaddrinfo() blocks indefinitely on RP2040/RP2350 when the
# CYW43 WiFi interface is active but has no IP address.
try:
import network
_wlan = network.WLAN(network.STA_IF)
if not _wlan.isconnected():
raise OSError(-1, "WiFi not connected")
except ImportError:
pass # Non-WiFi platform, skip guard
ai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
This is wrapped in try/except ImportError so it's safe on non-WiFi platforms (ESP32 Ethernet, Unix port, etc.).
Also consider
A default socket timeout (e.g., 30s) applied before getaddrinfo() would provide defense-in-depth, though it wouldn't help here since settimeout() only affects socket operations, not DNS resolution.
Environment
- Board: Raspberry Pi Pico 2 W (RP2350 + CYW43)
- MicroPython: v1.26.1
- Upstream issue: micropython/micropython#18797
extmod/modlwip: Fix getaddrinfo blocking indefinitely without network.
Summary
socket.getaddrinfo() blocks indefinitely on lwIP-based ports (confirmed on rp2/CYW43) when WiFi was previously connected then disconnected. This adds two fixes to the ERR_INPROGRESS case in lwip_getaddrinfo():
- Early-fail check: Before polling, verify at least one netif is up, has link, and has a valid IP. If not, fail immediately with
-2(consistent withlwip_getaddrinfo_cbfailure). This catches WiFi-inactive, active-but-never-connected, and disconnected-after-connection scenarios. - Safety-net timeout (20s): Guards against any other scenario where lwIP's internal DNS timeout doesn't fire.
Root Cause
Two factors combine to cause the indefinite hang:
-
Cached DNS servers: After WiFi disconnect, DNS server addresses from the previous DHCP lease remain cached in lwIP.
dns_gethostbyname()enqueues a query (returningERR_INPROGRESS) even though no network path exists. -
Soft timer self-disables: On the rp2 port, the lwIP soft timer switches to
ONE_SHOTmode when no netif hasLINK_UP(ports/rp2/mpnetworkport.c:150-161). Without the periodic timer,dns_tmr()never fires, so lwIP's built-in DNS retry/timeout mechanism is dead. The polling loop spins forever.
The early-fail check must verify three conditions (not just IP):
netif_is_up()— interface administratively upnetif_is_link_up()— physical link up (WiFi associated)!ip4_addr_isany_val()— valid IP (DHCP complete)
After disconnect, LINK_UP is cleared but the DHCP IP address lingers, so checking IP alone is insufficient.
Test Results
Tested on Raspberry Pi Pico 2 W (RP2350 + CYW43439), MicroPython v1.26.1:
| # | WiFi State | Expected | Result |
|---|---|---|---|
| 1 | IP literal, WiFi off | Resolves (ERR_OK path) | [(2,1,0,'',('10.0.0.1',80))] in 0ms |
| 2 | Connected | DNS resolves | 142.251.30.103 in 23ms |
| 3 | Was connected, then disconnect() |
OSError(-2) immediate |
OSError(-2) in 0ms |
| 4 | active(True), never connected |
OSError(-2) immediate |
OSError(-2) in 1ms |
| 5 | active(False) |
OSError(-2) immediate |
OSError(-2) in 1ms |
All 5/5 pass. Without the fix, tests 3 and 4 hang indefinitely.
Notes
- The fix is in
extmod/modlwip.cwhich is shared across all lwIP ports (rp2, esp32, etc.) - IP literal resolution and cached DNS results take the
ERR_OKpath and bypass this code entirely - The
-2status value is consistent with the existinglwip_getaddrinfo_cb()failure convention
Fixes #18797
This needs to be fixed properly in
socket.getaddrinfo()so it doesn't block indefinitely.