Troubleshooting HTTPRequest SSL and Timeout Issues with External Endpoints

I have been working on a lightweight Godot project that communicates with web services using the HTTPRequest node. Recently, I encountered an issue where fetching data and verifying responses from an external website results in inconsistent timeouts and failed handshakes. While standard browser tests indicate the endpoint is online, the engine frequently drops the connection during the TLS negotiation phase.

I am trying to determine if this is related to how SSL certificates or custom headers are processed within Godot 4. Has anyone experienced similar handshake failures when interacting with a dynamic website, and what is the best practice for configuring timeout and redirect settings to keep connections stable?

Three default values I measured on 4.7.1 stable first, because they bear directly on
“intermittent timeouts and handshake failures”:

A freshly constructed HTTPRequest: timeout = 0.0 (0 means it never times out),
use_threads = false, accept_gzip = true, max_redirects = 8, body_size_limit = -1,
download_chunk_size = 65536.

So: out of the box this node cannot time out. If nobody explicitly set timeout, what you
are calling a timeout isn’t one - it’s “the request never came back and nothing bounded it”,
which behaves exactly like the flakiness you describe. Step one is to write
http_request.timeout = 10.0 so an unbounded wait becomes a failure with a name (13 = RESULT_TIMEOUT).
Until you do, you can’t tell “the network is slow” apart from “the request was never sent”.

Step two: print the return value of request() too. I hit this myself and the symptom was
identical to yours. HTTPRequest holds only one in-flight request. Call request() again while
the previous one is still running and it does not queue and it does not crash - it just returns
44 (ERR_BUSY) and never sends. When I fired 5 URLs at the same node in a row, only the first
actually left; the other four all returned 44. If you don’t check that return value, that is
precisely what “occasional handshake failure” looks like.
(Worth adding: putting await request_completed inside a loop does not serialise the requests -
the coroutine hands control straight back to the caller. To go one-at-a-time, await _one(...)
itself, or use a fresh node per request.)

Step three: read the enum instead of guessing from numbers. The first argument of
request_completed is HTTPRequest.Result on 4.7.1, and there is exactly one TLS-shaped
failure: 5 = RESULT_TLS_HANDSHAKE_ERROR. 2 CANT_CONNECT / 3 CANT_RESOLVE /
4 CONNECTION_ERROR / 6 NO_RESPONSE / 13 TIMEOUT are different illnesses, and separating them is
what tells you whether to look at the certificate chain, at DNS, or at the endpoint itself.
I verified against a self-signed host on this machine and it did come back as 5, so the code is real.

If it really is 5, there are two switches on the Godot side (the names below are what I found
scanning project settings on 4.7.1 - don’t substitute anything else
):

  • network/tls/enable_tls_v1.3 - turning it off forces 1.2, which is how you test whether
    version negotiation is the step that fails;
  • network/tls/certificate_bundle_override - point it at your own CA bundle.
    A server with an incomplete intermediate chain works in a browser because the browser finishes
    the chain itself; the engine doesn’t. That is the usual cause of “fine in a browser, fails in
    the engine”. Note I did not find the Godot 3 setting named certificate_store in 4.7.1,
    so don’t follow old tutorials looking for it. (One boundary: if this is a web export, the
    browser performs the TLS handshake and neither setting participates. I did not measure web-side
    behaviour here, so don’t apply the desktop conclusions to the web build.)
    A pure-timeout knob lives nearby too: network/limits/tcp/connect_timeout_seconds.

One last unglamorous but common possibility: you may be waiting on the network from the main
thread
. use_threads defaults to false, so requests advance a little at a time inside the main
loop. If that loop also has heavy work in it (terrain generation, mass instantiation), the polling
after the ClientHello goes out gets stalled and the peer times out against a server that never
looked busy. Test it once: turn use_threads = true and run the same batch. If the intermittent
failures disappear with it, the problem is main-loop stalling, not SSL.
(That one applies to desktop and Android. I did not measure it for web exports - see the
hidden-tab trap where the main loop gets no frames at all.)