Skip to content

Instantly share code, notes, and snippets.

@Darkyenus
Last active January 15, 2026 11:04
Show Gist options
  • Select an option

  • Save Darkyenus/aa196349815bdf12a5dfcc4d45a608cd to your computer and use it in GitHub Desktop.

Select an option

Save Darkyenus/aa196349815bdf12a5dfcc4d45a608cd to your computer and use it in GitHub Desktop.
Simple async GDScript HTTP request method
class_name HTTPResponse extends RefCounted
## The response HTTP code, -1 on error
var http_code: int = -1;
## The response headers
var http_headers: PackedStringArray = [];
## The response body as bytes
var http_response_body: PackedByteArray = [];
## Whether the response body has been read completely or if there is a part missing
var http_response_body_complete: bool = true;
func get_body_as_string() -> String:
assert(http_response_body_complete);
# TODO Check encoding from headers
return http_response_body.get_string_from_utf8();
static func request_async(url: String, headers: Array[String] = [], body: PackedByteArray = [], method: HTTPClient.Method = HTTPClient.Method.METHOD_GET) -> HTTPResponse:
assert(url.begins_with("http://") || url.begins_with("https://"));
var client: HTTPClient = HTTPClient.new();
var host_end: int = url.find("/", "https://".length());
var host: String;
var path: String;
if host_end == -1:
host = url;
path = "/";
else:
host = url.substr(0, host_end);
path = url.substr(host_end);
print_debug("request(%s | %s, %s, %d bytes, %s)" % [host, path, headers, body.size(), method]);
var status: HTTPClient.Status = client.get_status();
if _is_bad(client.connect_to_host(host), "connect_to_host", host):
return HTTPResponse.new();
while true:
status = await _await_next_status(host, client, status);
if _is_status_error(status):
return;
if status == HTTPClient.STATUS_CONNECTING || status == HTTPClient.STATUS_RESOLVING:
continue;
if status == HTTPClient.STATUS_CONNECTED:
break;
printerr("request(%s) unexpected status" % host);
return;
if _is_bad(client.request_raw(method, path, headers as PackedStringArray, body) as Error, "request_raw", host):
return HTTPResponse.new();
while true:
status = await _await_next_status(host, client, status);
if _is_status_error(status):
return;
if status == HTTPClient.STATUS_REQUESTING:
continue;
if status == HTTPClient.STATUS_BODY || status == HTTPClient.STATUS_CONNECTED:
break;
printerr("request(%s) unexpected status" % host);
return HTTPResponse.new();
var response_code: int = client.get_response_code();
var response_headers: PackedStringArray = client.get_response_headers();
var response_content_length: int = client.get_response_body_length();
print_debug("request(%s) %d %s (%d bytes)" % [host, response_code, response_headers, response_content_length]);
var redirect_location: Variant = _get_header(response_headers, "Location");
if redirect_location != null && method != HTTPClient.METHOD_CONNECT:
if response_code == 303:
print_debug("request(%s) redirecting to %s" % [host, redirect_location]);
return await request_async(redirect_location, headers, [], HTTPClient.METHOD_GET);
if response_code == 301 || response_code == 302 || response_code == 307 || response_code == 308:
print_debug("request(%s) redirecting to %s" % [host, redirect_location]);
return await request_async(redirect_location, headers, [], method);
var result: HTTPResponse = HTTPResponse.new();
result.http_code = response_code;
result.http_headers = response_headers;
if status == HTTPClient.STATUS_BODY:
result.http_response_body_complete = false;
while true:
var chunk: PackedByteArray = client.read_response_body_chunk();
result.http_response_body.append_array(chunk);
status = client.get_status();
if status == HTTPClient.STATUS_CONNECTED || status == HTTPClient.STATUS_DISCONNECTED:
result.http_response_body_complete = true;
break;
elif status == HTTPClient.STATUS_BODY:
continue;
else:
printerr("request(%s) failed to download the full body, stuck on %s" % [host, _status_string(status)]);
break;
return result;
static func _status_string(status: HTTPClient.Status) -> String:
match status:
HTTPClient.STATUS_DISCONNECTED: return "STATUS_DISCONNECTED";
HTTPClient.STATUS_RESOLVING: return "STATUS_RESOLVING";
HTTPClient.STATUS_CANT_RESOLVE: return "STATUS_CANT_RESOLVE";
HTTPClient.STATUS_CONNECTING: return "STATUS_CONNECTING";
HTTPClient.STATUS_CANT_CONNECT: return "STATUS_CANT_CONNECT";
HTTPClient.STATUS_CONNECTED: return "STATUS_CONNECTED";
HTTPClient.STATUS_REQUESTING: return "STATUS_REQUESTING";
HTTPClient.STATUS_BODY: return "STATUS_BODY";
HTTPClient.STATUS_CONNECTION_ERROR: return "STATUS_CONNECTION_ERROR";
HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: return "STATUS_TLS_HANDSHAKE_ERROR";
return "UnknownStatus(%d)" % status;
static func _is_status_error(status: HTTPClient.Status) -> bool:
match status:
HTTPClient.STATUS_DISCONNECTED: return true;
HTTPClient.STATUS_RESOLVING: return false;
HTTPClient.STATUS_CANT_RESOLVE: return true;
HTTPClient.STATUS_CONNECTING: return false;
HTTPClient.STATUS_CANT_CONNECT: return true;
HTTPClient.STATUS_CONNECTED: return false;
HTTPClient.STATUS_REQUESTING: return false;
HTTPClient.STATUS_BODY: return false;
HTTPClient.STATUS_CONNECTION_ERROR: return true;
HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: return true;
return true;
static func _await_next_status(host: String, client: HTTPClient, old_status: HTTPClient.Status) -> HTTPClient.Status:
var tree: SceneTree = (Engine.get_main_loop() as SceneTree);
while true:
if _is_bad(client.poll(), "poll", host):
break;
var new_status: HTTPClient.Status = client.get_status();
if old_status != new_status:
print_debug("request(%s) new status: %s" % [host, _status_string(new_status)]);
return new_status;
await tree.create_timer(0.05, true, false, true).timeout;
return HTTPClient.Status.STATUS_CONNECTION_ERROR;
static func _is_bad(error: Error, context: String, host: String) -> bool:
if error == Error.OK:
return false;
printerr("request(%s) error in %s: %s" % [host, context, error_string(error)]);
return true;
static func _get_header(headers: PackedStringArray, header: String) -> Variant:
for h: String in headers:
if h.length() >= header.length() + 2 && h.begins_with(header) && h[header.length()] == ':' && h[header.length() + 1] == ' ':
return h.substr(header.length() + 2);
return null;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment