Last active
January 15, 2026 11:04
-
-
Save Darkyenus/aa196349815bdf12a5dfcc4d45a608cd to your computer and use it in GitHub Desktop.
Simple async GDScript HTTP request method
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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