Quick answer: HTTPRequest must be added to the scene tree to function, its request_completed signal must be connected before calling request(), and the timeout property defaults to 0 (no timeout), which causes indefinite hangs on unreachable servers. Set a timeout, check SSL settings, and verify CORS headers for HTML5 exports.

The HTTPRequest node is Godot’s built-in tool for making HTTP calls, but it has several non-obvious requirements that cause it to silently hang or never fire its completion signal. Unlike some APIs that return an error immediately when misconfigured, a misconfigured HTTPRequest often just does nothing — forever. Here’s a systematic guide to diagnosing and fixing every common failure mode.

HTTPRequest Must Be in the Scene Tree

The most surprising requirement for HTTPRequest is that it must be added to the scene tree to work. An HTTPRequest node that is instantiated in code but never added to the tree as a child of another node will not process network responses. The node relies on Godot’s process loop to drive its internal state machine, and nodes outside the scene tree don’t receive process ticks.

# WRONG: instantiated but not added to scene tree
func fetch_leaderboard():
    var req = HTTPRequest.new()
    req.request("https://api.example.com/scores")
    # request_completed never fires — req is not in the tree

# CORRECT: add to the scene tree before making the request
func fetch_leaderboard():
    var req = HTTPRequest.new()
    add_child(req)
    req.request_completed.connect(_on_leaderboard_completed)
    req.request("https://api.example.com/scores")

When creating HTTPRequest nodes dynamically, also consider freeing them after use to avoid leaking nodes. Connect to the request_completed signal, and in the callback, call queue_free() on the request node once you’ve processed the response:

func _on_leaderboard_completed(result, response_code, headers, body):
    if result == HTTPRequest.RESULT_SUCCESS:
        var data = JSON.parse_string(body.get_string_from_utf8())
        update_leaderboard_ui(data)
    else:
        push_error("Request failed with result code: " + str(result))

    # Clean up the dynamically created node
    $LeaderboardRequest.queue_free()

The request_completed Signal Must Be Connected First

The request_completed signal carries all response data: the result code, HTTP status code, response headers, and body. If this signal is not connected before request() is called, the response is delivered and discarded. The connection must exist when the response arrives, not just before you expect to need it.

# Recommended pattern: connect signal, then request
@onready var http_request: HTTPRequest = $HTTPRequest

func _ready():
    # Connect in _ready so the signal is ready for any call
    http_request.request_completed.connect(_on_request_completed)

func fetch_player_data(player_id: String):
    var url = "https://api.example.com/players/" + player_id
    var err = http_request.request(url)
    if err != OK:
        push_error("HTTPRequest.request() failed: " + error_string(err))

func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
    print("Result: ", result, " HTTP status: ", response_code)
    if result != HTTPRequest.RESULT_SUCCESS:
        push_error("Network error, result code: " + str(result))
        return
    # response_code is the HTTP status (200, 404, 500, etc.)
    if response_code != 200:
        push_warning("Unexpected HTTP status: " + str(response_code))
        return
    var json = JSON.parse_string(body.get_string_from_utf8())
    handle_player_data(json)

Note the distinction between result and response_code. result is a Godot-level enum describing the transport outcome (RESULT_SUCCESS, RESULT_CONNECTION_ERROR, RESULT_SSL_HANDSHAKE_ERROR, RESULT_TIMEOUT, etc.). response_code is the HTTP status code from the server (200, 404, 500). A successful request can return a non-200 response code — check both.

The timeout Property Defaults to 0 (No Timeout)

By default, HTTPRequest.timeout is 0.0, which means the request will wait indefinitely for a response. If the server is unreachable, the DNS lookup fails silently, or the connection hangs at the TCP level, your game will wait forever with no callback firing. This is the most common cause of “HTTPRequest never completes.”

Always set a reasonable timeout. For most game API calls, 10–30 seconds is appropriate. For real-time features (presence, matchmaking), use shorter timeouts (5–10 seconds) so the game can fail fast and show an error to the player:

func _ready():
    http_request.request_completed.connect(_on_request_completed)
    # Set timeout in seconds; RESULT_TIMEOUT fires if exceeded
    http_request.timeout = 10.0

func _on_request_completed(result, response_code, headers, body):
    if result == HTTPRequest.RESULT_TIMEOUT:
        push_warning("Request timed out after 10 seconds")
        show_network_error_ui()
        return
    # Handle other results...

SSL/TLS Certificate Verification Failures

When connecting to an HTTPS server, Godot verifies the server’s TLS certificate against a bundle of trusted certificate authorities. This verification can fail for several reasons: the server uses a self-signed certificate, the server’s certificate chain is incomplete, or the Godot export doesn’t include the CA bundle.

The result code for TLS failure is RESULT_SSL_HANDSHAKE_ERROR (value 6). You can also see this as a “Handshake with server failed” error in the Godot debugger console.

For production servers with valid certificates, the fix is to ensure your export template includes the certificate bundle (check the Export dialog settings). For development servers with self-signed certificates, use TLSOptions to configure client-side trust:

# For development with self-signed certificates only —
# never disable verification for production or player-facing features
func make_dev_request(url: String):
    var tls_options = TLSOptions.client_unsafe()  # skips cert verification
    http_request.request(url, [], HTTPClient.METHOD_GET, "", tls_options)

# For production with a custom CA (e.g. internal game services):
func make_request_with_custom_ca(url: String):
    var ca_cert = load("res://certs/my_ca.crt")
    var tls_options = TLSOptions.client(ca_cert)
    http_request.request(url, [], HTTPClient.METHOD_GET, "", tls_options)

Cancelling In-Flight Requests

If the player changes screens, quits, or triggers a new request before the previous one completes, you need to cancel the in-flight request. HTTPRequest.cancel_request() aborts the current request and resets the node so it can accept a new one. Without it, starting a second request while one is in progress is undefined behaviour in older Godot versions and causes a silent failure in newer ones.

var is_requesting: bool = false

func fetch_data(url: String):
    # Cancel previous request if still in flight
    if is_requesting:
        http_request.cancel_request()
        is_requesting = false

    is_requesting = true
    http_request.request(url)

func _on_request_completed(result, response_code, headers, body):
    is_requesting = false
    # Handle result...

HTML5 Export: CORS Restrictions

When exporting your game to HTML5 and running it in a browser, all HTTP requests go through the browser’s fetch/XHR infrastructure, which enforces CORS (Cross-Origin Resource Sharing). A request from https://itch.io/embed/... to https://api.yourgame.com is a cross-origin request and will be blocked by the browser if your server does not respond with the correct CORS headers.

The result in Godot is typically RESULT_CONNECTION_ERROR with no useful detail, because the browser blocks the request before any response body is available. The actual error appears only in the browser’s developer console.

The fix must be on the server side: add Access-Control-Allow-Origin headers to your API responses. For testing, you can use a wildcard, but production deployments should specify exact allowed origins:

# Server response headers needed for HTML5 export (server-side config)
# Access-Control-Allow-Origin: https://yourgame.itch.io
# Access-Control-Allow-Methods: GET, POST, OPTIONS
# Access-Control-Allow-Headers: Content-Type, Authorization

# In Godot: detect the HTML5 export and log extra context
func _on_request_completed(result, response_code, headers, body):
    if result == HTTPRequest.RESULT_CONNECTION_ERROR:
        if OS.get_name() == "Web":
            push_warning("Connection error on Web export — check browser console for CORS errors")
        else:
            push_error("Connection refused or network error")

Also note that the HTML5 export does not support all HTTP features available on desktop. Notably, some lower-level socket operations and certain HTTP headers cannot be set from the browser context. If you need full HTTP control in your HTML5 game, a proxy server on your own domain eliminates the CORS problem entirely by making the requests server-to-server.

Checklist: When HTTPRequest Hangs or Never Completes

“An HTTPRequest node that is not in the scene tree is an expensive way to do nothing. Always add it as a child before calling request().”

Set a timeout, connect the signal, add the node to the tree — in that order — and most HTTPRequest mysteries solve themselves.