From b3d30f44e5cca977c958c85a7a818cdf4cf5fb91 Mon Sep 17 00:00:00 2001 From: MrObvious Date: Wed, 29 Jul 2026 16:19:33 -0500 Subject: [PATCH 1/2] https_client: trigger connection reset timer on HTTP/2 and send/recv errors, not just timeouts We saw recurring "curl request failed with 16: Error" (CURLE_HTTP2) and "curl request failed with 55: Error / Send failure: Broken pipe" (CURLE_SEND_ERROR) on a production OpenWrt router running two https-dns-proxy instances, even after tuning max_idle_time down to 30s. These are classic symptoms of curl reusing a stale/half-closed HTTP/2 connection - the same class of problem the existing reset_timer/https_client_reset mechanism was built to recover from, but that mechanism is currently only armed on CURLE_OPERATION_TIMEDOUT. HTTP/2 stream errors and send/recv errors on a reused connection never reach it, so the proxy keeps trying to reuse a bad connection indefinitely instead of forcing a fresh one. This extends the existing case in https_fetch_ctx_process_response() to also arm the reset timer for CURLE_HTTP2, CURLE_HTTP2_STREAM, CURLE_GOT_NOTHING, and CURLE_SEND_ERROR - all indicators of a broken/stale connection rather than a one-off content error. No new mechanism is introduced; this only widens the set of error codes that trigger the recovery path that already exists. Tested on a production OpenWrt router (two instances, Cloudflare + Quad9 backends) - confirmed the reset timer now arms and fires (full client reset) in response to these errors, where previously they were silently ignored. --- src/https_client.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/https_client.c b/src/https_client.c index 01605a1..baa7d5a 100644 --- a/src/https_client.c +++ b/src/https_client.c @@ -357,6 +357,12 @@ static int https_fetch_ctx_process_response(https_client_t *client, WLOG_REQ("curl request failed with write error (probably response content was too large)"); break; case CURLE_OPERATION_TIMEDOUT: + case CURLE_HTTP2: + case CURLE_HTTP2_STREAM: + case CURLE_GOT_NOTHING: + case CURLE_SEND_ERROR: + // These all indicate a stale/broken (often reused HTTP/2) connection, + // not a one-off content error - same recovery path as a timeout. if (!ev_is_active(&client->reset_timer)) { ILOG_REQ("Client reset timer started"); ev_timer_start(client->loop, &client->reset_timer); From 9d7dc61cc363e4134eb8269a1dc314d837497747 Mon Sep 17 00:00:00 2001 From: MrObvious Date: Wed, 29 Jul 2026 16:23:04 -0500 Subject: [PATCH 2/2] https_client: log recoverable connection errors at INFO instead of WARNING Follow-up to #210 (reset-timer fix). Submitted as a separate change since this is a logging-policy question rather than a behavior fix, and should not hold up the reset-timer fix if there is disagreement on log levels. CURLE_OPERATION_TIMEDOUT, CURLE_HTTP2, CURLE_HTTP2_STREAM, CURLE_GOT_NOTHING, and CURLE_SEND_ERROR are all connection-reuse hiccups that the reset-timer path (see #210) already recovers from automatically without any user-visible impact. On resolvers whose edge cycles persistent connections periodically (observed with Quad9, not seen with Cloudflare), this is a routine, expected, self-healing condition - not a warning-worthy event - yet it was logged at WARNING twice per occurrence. On flash- and memory-constrained routers running at default verbosity, this produces a steady stream of log noise for a condition the proxy is already handling correctly on its own. This logs the same information at INFO instead of WARNING specifically for the recoverable error codes above, matching the existing INFO-level "Client reset timer started" message already used for this recovery path. Genuinely unexpected failures are unchanged and still log at WARNING. Tested on the same production router as #210 - confirmed curl-16 spam disappears from default-verbosity logs while the reset-timer recovery (and its own INFO-level messages, visible at raised verbosity) continues to function. --- src/https_client.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/https_client.c b/src/https_client.c index baa7d5a..0427721 100644 --- a/src/https_client.c +++ b/src/https_client.c @@ -347,6 +347,11 @@ static int https_fetch_ctx_process_response(https_client_t *client, long long_resp = 0; char *str_resp = NULL; int faulty_response = 1; + // Set for connection-reuse hiccups that the reset-timer path already + // recovers from automatically - these are routine, not warning-worthy, + // and logging them at WARNING just spams flash-constrained routers with + // an expected condition. Genuinely unexpected failures still log at WARNING. + int recoverable = 0; switch (curl_result_code) { case CURLE_OK: @@ -363,11 +368,17 @@ static int https_fetch_ctx_process_response(https_client_t *client, case CURLE_SEND_ERROR: // These all indicate a stale/broken (often reused HTTP/2) connection, // not a one-off content error - same recovery path as a timeout. + recoverable = 1; if (!ev_is_active(&client->reset_timer)) { ILOG_REQ("Client reset timer started"); ev_timer_start(client->loop, &client->reset_timer); } - __attribute__((fallthrough)); + ILOG_REQ("curl request failed with %d: %s (recoverable, reset timer will recycle connection)", + curl_result_code, curl_easy_strerror(curl_result_code)); + if (ctx->curl_errbuf[0] != 0) { + ILOG_REQ("curl error message: %s", ctx->curl_errbuf); + } + break; default: WLOG_REQ("curl request failed with %d: %s", curl_result_code, curl_easy_strerror(curl_result_code)); if (ctx->curl_errbuf[0] != 0) { @@ -385,8 +396,8 @@ static int https_fetch_ctx_process_response(https_client_t *client, curl_off_t uploaded_bytes = 0; if (curl_easy_getinfo(ctx->curl, CURLINFO_SIZE_UPLOAD_T, &uploaded_bytes) == CURLE_OK && uploaded_bytes > 0) { - WLOG_REQ("Connecting and sending request to resolver was successful, " - "but no response was sent back"); + LOG(recoverable ? LOG_INFO : LOG_WARNING, "%04hX: Connecting and sending request to resolver was successful, " + "but no response was sent back", ctx->id); if (client->opt->use_http_version == 1) { // for example Unbound DoH servers does not support HTTP/1.x, only HTTP/2 WLOG("Resolver may not support current HTTP/%s protocol version", @@ -398,7 +409,7 @@ static int https_fetch_ctx_process_response(https_client_t *client, // that have been opened a long time ago (if CURLOPT_MAXAGE_CONN can not be increased // it is 118 seconds) // also: when no internet connection, this floods the log for every failed request - WLOG_REQ("No response (probably connection has been closed or timed out)"); + LOG(recoverable ? LOG_INFO : LOG_WARNING, "%04hX: No response (probably connection has been closed or timed out)", ctx->id); } } else { WLOG_REQ("curl response code: %d, content length: %zu", long_resp, ctx->buflen);