Quick answer: The most common cause is that the HTTPRequest node is not in the scene tree when the request is made. HTTPRequest requires an active scene tree to process its internal polling. If you create the node dynamically with HTTPRequest.new() but forget to call add_child(), the request will never complete.
Here is how to fix Godot HTTP request not completing. You fire an HTTP request in Godot 4, and nothing happens. No signal, no error, no timeout—just silence. The request_completed callback never fires. This is one of the most common networking issues in Godot, and it has several distinct causes that each require a different fix. This guide covers every reason your HTTPRequest node might be failing silently and how to resolve each one.
The HTTPRequest Node Must Be in the Scene Tree
The single most common cause of HTTPRequest doing nothing is that the node is not in the scene tree. Unlike a plain HTTPClient, the HTTPRequest node relies on the scene tree’s process loop to poll the underlying connection. If the node is orphaned, the request is initiated at the socket level but never polled for completion.
This happens most often when you create an HTTPRequest dynamically:
# This will NOT work — node is not in the tree
var http := HTTPRequest.new()
http.request("https://api.example.com/data")
# This WILL work — node is added to the tree first
var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(_on_request_completed)
http.request("https://api.example.com/data")
If you are adding the HTTPRequest as a child node in the editor, verify that the parent node is in the active scene. A common mistake is to have the HTTPRequest attached to a node that gets freed or removed from the tree before the request finishes.
You can confirm the node is in the tree by adding a diagnostic check:
func make_request(url: String) -> void:
if not http_request.is_inside_tree():
push_error("HTTPRequest node is not in the scene tree")
return
http_request.request(url)
SSL/TLS Certificate Issues
If your request targets an HTTPS endpoint and the TLS handshake fails, Godot 4 will often fail silently or return a RESULT_TLS_HANDSHAKE_ERROR that you might miss if you are not checking the result code in your callback.
In Godot 4, TLS handling changed significantly from Godot 3. The old use_ssl parameter was replaced with a tls_options property. For most public APIs using certificates from well-known CAs, HTTPS works out of the box. Problems arise with self-signed certificates, internal APIs, or certain hosting providers.
# For development with self-signed certs (NEVER ship this)
var tls := TLSOptions.client_unsafe()
http_request.tls_options = tls
# For production with a custom CA certificate
var cert := X509Certificate.new()
cert.load("res://certs/custom_ca.crt")
var tls := TLSOptions.client(cert)
http_request.tls_options = tls
If you are testing locally against a development server, the most common fix is to either use HTTP (not HTTPS) during development or use TLSOptions.client_unsafe() temporarily. Remember to remove the unsafe option before shipping.
To diagnose TLS issues, always check the result code in your callback:
func _on_request_completed(result: int, code: int,
headers: PackedStringArray, body: PackedByteArray) -> void:
match result:
HTTPRequest.RESULT_SUCCESS:
var json := JSON.parse_string(body.get_string_from_utf8())
print("Response: ", json)
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
push_error("TLS handshake failed — check certificate")
HTTPRequest.RESULT_CONNECTION_ERROR:
push_error("Connection failed — server unreachable")
HTTPRequest.RESULT_TIMEOUT:
push_error("Request timed out")
_:
push_error("HTTPRequest failed with result: %d" % result)
Timeout Configuration
By default, Godot’s HTTPRequest has no timeout. This means a request to an unresponsive server will hang indefinitely, and your callback will never fire. This is a design choice that trips up many developers.
Always set an explicit timeout:
@onready var http_request := $HTTPRequest
func _ready() -> void:
# Set timeout to 15 seconds
http_request.timeout = 15.0
http_request.request_completed.connect(_on_request_completed)
func fetch_data() -> void:
var error := http_request.request("https://api.example.com/data")
if error != OK:
push_error("Failed to initiate request: %d" % error)
Choose your timeout value based on what the request is doing. API calls to a fast backend: 10–15 seconds. File uploads or large downloads: 60–120 seconds. Analytics pings that should not block gameplay: 5 seconds.
When a timeout occurs, the request_completed signal fires with RESULT_TIMEOUT. You can use this to implement retry logic:
var retry_count := 0
const MAX_RETRIES := 3
func _on_request_completed(result: int, code: int,
headers: PackedStringArray, body: PackedByteArray) -> void:
if result == HTTPRequest.RESULT_TIMEOUT and retry_count < MAX_RETRIES:
retry_count += 1
push_warning("Request timed out, retry %d/%d" % [retry_count, MAX_RETRIES])
fetch_data()
return
retry_count = 0
# Handle success or final failure
Redirect Handling and max_redirects
HTTPRequest follows redirects automatically, but only up to a limit. The default max_redirects value is 8. If your server returns more redirects than this (or if there is a redirect loop), the request will fail.
A subtle version of this problem occurs with URLs that need a trailing slash. Some servers redirect /api/data to /api/data/, consuming one redirect. If there are authentication redirects on top of that, you can exhaust the limit unexpectedly.
# Increase redirect limit if needed
http_request.max_redirects = 16
# Or disable redirects entirely and handle them manually
http_request.max_redirects = 0
Watch for HTTP 301/302 responses. If your request completes but you get an unexpected HTML page instead of JSON, you are likely being redirected to a login page or error page. Check the code parameter in your callback—a redirect that was not followed will show up as a 301 or 302 status code with the redirect target in the Location header.
Thread Safety and Concurrent Requests
Each HTTPRequest node can only handle one request at a time. If you call request() while a previous request is still in flight, the new request will fail with ERR_BUSY. This is a common issue in games that fire multiple API calls in quick succession—for example, submitting a bug report while an analytics ping is in progress.
# BAD: reusing the same HTTPRequest for concurrent calls
func send_analytics() -> void:
http_request.request("https://api.example.com/analytics")
func send_bug_report() -> void:
# This will fail if analytics request is still pending
http_request.request("https://api.example.com/bugs")
# GOOD: create a fresh HTTPRequest for each concurrent call
func send_request(url: String, callback: Callable) -> void:
var http := HTTPRequest.new()
http.timeout = 15.0
add_child(http)
http.request_completed.connect(
func(result, code, headers, body):
callback.call(result, code, body)
http.queue_free()
)
http.request(url)
The pattern of creating a new HTTPRequest per call and freeing it in the callback is clean and avoids busy errors. Just be careful not to create hundreds of simultaneous requests—each one opens a socket and consumes resources.
“Nine times out of ten, when someone reports that HTTPRequest is broken in Godot, the node either is not in the tree or they are missing the timeout setting. The actual HTTP stack in Godot is solid once you get the configuration right.”
Debugging Checklist
When an HTTPRequest is not working, run through this checklist in order:
1. Is the node in the scene tree? Call is_inside_tree() right before the request. If it returns false, add the node to the tree.
2. Did the request() call return OK? The request() method returns an error code immediately. If it returns anything other than OK (0), the request was never started. Common error codes: ERR_BUSY (another request is active), ERR_INVALID_PARAMETER (malformed URL), ERR_CANT_CONNECT (DNS or network failure).
3. Is the timeout set? If you are waiting forever for a response, add http_request.timeout = 15.0.
4. Are you handling all result codes? The request_completed signal fires for failures too. Always check the result parameter before accessing the body.
5. Is the URL correct? Test the URL in a browser or with curl first. Verify HTTPS vs HTTP, check for trailing slashes, confirm the server is running.
6. Are you on the main thread? HTTPRequest must be used from the main thread. If you are making requests from a background thread, use call_deferred() to marshal back to the main thread.
Related Issues
If your HTTP requests work but your save data does not persist, see Fix: Godot Save/Load Game Data Not Persisting. For adding bug reporting that handles all these HTTP edge cases for you, check out Bug Reporting Tools for Godot Developers. For a broader look at Godot networking patterns, read How to Add a Bug Reporter to a Godot Game.
Most HTTPRequest problems are configuration issues, not bugs. Set a timeout, verify the node is in the tree, and check every error code. The Godot networking stack is reliable once you give it the right setup.