└── addons └── awaitable_http_request ├── icon.png ├── plugin.cfg ├── plugin.gd ├── LICENSE ├── awaitable_http_request.gd ├── examples.tscn └── http_result.gd /addons/awaitable_http_request/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swarkin/Godot-AwaitableHTTPRequest/HEAD/addons/awaitable_http_request/icon.png -------------------------------------------------------------------------------- /addons/awaitable_http_request/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="AwaitableHTTPRequest Node" 4 | description="This addon makes HTTP requests much more convenient to use by introducing the await-syntax and removing the need for signals." 5 | author="Swarkin" 6 | version="2.2.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/awaitable_http_request/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | add_custom_type("AwaitableHTTPRequest", "HTTPRequest", preload("awaitable_http_request.gd"), preload("icon.png")) 7 | add_custom_type("HTTPResult", "RefCounted", preload("http_result.gd"), preload("icon.png")) 8 | 9 | func _exit_tree(): 10 | remove_custom_type("AwaitableHTTPRequest") 11 | remove_custom_type("HTTPResult") 12 | -------------------------------------------------------------------------------- /addons/awaitable_http_request/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Swarkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/awaitable_http_request/awaitable_http_request.gd: -------------------------------------------------------------------------------- 1 | class_name AwaitableHTTPRequest 2 | extends HTTPRequest 3 | ## [img width=64]res://addons/awaitable_http_request/icon.png[/img] [url=https://github.com/Swarkin/Godot-AwaitableHTTPRequest]AwaitableHTTPRequest[/url] 2.2.0 by Swarkin & [url=https://github.com/Swarkin/Godot-AwaitableHTTPRequest/graphs/contributors]contributors[/url]. 4 | # View the formatted documentation in Godot by pressing F1 and typing "AwaitableHTTPRequest"! 5 | 6 | signal request_finished ## Emits once the current request finishes, right after [member is_requesting] is set to false. 7 | var is_requesting := false ## Whether the node is busy performing a request. This variable is read-only. 8 | 9 | ## Performs an awaitable HTTP request.[br] 10 | ## Take a look at the [code]examples.tscn[/code] scene in the addon directory for inspiration![br] 11 | ## [br] 12 | ## [b]Note:[/b] Header names will be in lowercase, as some web servers prefer this approach and them being case-insensitive as per specification. Therefore, it is good practice to not rely on capitalization. 13 | ## [br] 14 | ## Here is an example with minimal error-handling: 15 | ## [codeblock] 16 | ## extends AwaitableHTTPRequest 17 | ## 18 | ## func _ready() -> void: 19 | ## var resp := await async_request("https://api.github.com/users/swarkin") 20 | ## if resp.success() and resp.status_ok(): 21 | ## print(resp.status) # 200 22 | ## print(resp.headers["content-type"]) # application/json 23 | ## 24 | ## var json := resp.body_as_json() 25 | ## print(json["login"]) # Swarkin 26 | ## [/codeblock] 27 | func async_request(url: String, custom_headers := PackedStringArray(), method := HTTPClient.Method.METHOD_GET, request_data := "") -> HTTPResult: 28 | if is_requesting: 29 | push_warning("AwaitableHTTPRequest is busy performing a request.") 30 | return HTTPResult._from_error(Error.ERR_BUSY) 31 | 32 | is_requesting = true 33 | 34 | var err := request(url, custom_headers, method, request_data) 35 | if err: 36 | return HTTPResult._from_error(err) 37 | 38 | @warning_ignore("unsafe_cast") 39 | var result := await request_completed as Array 40 | is_requesting = false 41 | request_finished.emit() 42 | 43 | return HTTPResult._from_array(result) 44 | -------------------------------------------------------------------------------- /addons/awaitable_http_request/examples.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bnqsp15ey3wrl"] 2 | 3 | [ext_resource type="Script" path="res://addons/awaitable_http_request/awaitable_http_request.gd" id="1_b05hb"] 4 | 5 | [sub_resource type="GDScript" id="GDScript_qphse"] 6 | resource_name = "example" 7 | script/source = "extends Node 8 | 9 | @export var http: AwaitableHTTPRequest 10 | 11 | 12 | func _ready() -> void: 13 | print(\"Example 1: JSON API\") 14 | var data := await request_api() 15 | if not data.is_empty(): 16 | var user := data[\"login\"] as String 17 | print(\"User: \", user) 18 | 19 | print(\"\\nExample 2: Downloading an image\") 20 | var bytes := await request_image() 21 | if not bytes.is_empty(): 22 | # Snippet for loading a PackedByteArray into an Image, 23 | # as well as an ImageTexture to use in your app/game. 24 | #var img := Image.new() 25 | #img.load_png_from_buffer(bytes) 26 | #var tex := ImageTexture.create_from_image(img) 27 | 28 | var path := OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)+\"/image.png\" 29 | var file := FileAccess.open(path, FileAccess.WRITE) 30 | if not file: 31 | push_error(\"Failed to save image.\") 32 | return 33 | 34 | file.store_buffer(bytes) 35 | print(\"Downloaded and saved a random image to %s, take a look!\" % path) 36 | 37 | 38 | #region Example 1: JSON API 39 | func request_api() -> Dictionary: 40 | var resp := await http.async_request( 41 | \"https://api.github.com/users/swarkin\", 42 | PackedStringArray([ # headers 43 | \"accept: application/vnd.github+json\", 44 | \"user-agent: Swarkin/AwaitableHTTPRequest/2.2.0\", 45 | ]), 46 | ) 47 | 48 | if !resp.success() or resp.status_err(): 49 | push_error(\"Request failed.\") 50 | return {} 51 | 52 | print(\"Status code: \", resp.status) 53 | print(\"Content-Type:\", resp.headers[\"content-type\"]) 54 | 55 | var json := resp.body_as_json() 56 | if not json: 57 | push_error(\"JSON invalid.\") 58 | return {} 59 | 60 | return json as Dictionary 61 | #endregion 62 | 63 | #region Example 2: Downloading an image 64 | func request_image() -> PackedByteArray: 65 | var resp := await http.async_request(\"https://picsum.photos/256\") 66 | if !resp.success() or resp.status_err(): 67 | push_error(\"Request failed.\") 68 | return PackedByteArray() 69 | 70 | return resp.bytes 71 | #endregion 72 | " 73 | 74 | [node name="Press F6 to run examples" type="Node" node_paths=PackedStringArray("http")] 75 | editor_description = "This scene is not required and may be deleted freely." 76 | script = SubResource("GDScript_qphse") 77 | http = NodePath("AwaitableHTTPRequest") 78 | 79 | [node name="AwaitableHTTPRequest" type="HTTPRequest" parent="."] 80 | script = ExtResource("1_b05hb") 81 | -------------------------------------------------------------------------------- /addons/awaitable_http_request/http_result.gd: -------------------------------------------------------------------------------- 1 | class_name HTTPResult 2 | extends RefCounted 3 | ## A dataclass returned by [method AwaitableHTTPRequest.async_request]. 4 | 5 | var _error: Error ## Contains the [method HTTPRequest.request] error, [constant Error.OK] otherwise. See also [method success].[br](For advanced use-cases) 6 | var _result: HTTPRequest.Result ## Contains the [annotation HTTPRequest] error, [constant HTTPRequest.RESULT_SUCCESS] otherwise. See also [method success].[br](For advanced use-cases) 7 | var status: int ## The response status code. 8 | var headers: Dictionary ## The response headers. 9 | var bytes: PackedByteArray ## The response body as a [PackedByteArray].[br][b]Note:[/b] Any [Array] is always passed by reference. 10 | 11 | ## Checks whether the HTTP request succeeded, meaning [member _error] and [member _result] aren't in an error state.[br] 12 | ## [b]Note:[/b] This does not check the response [member status] code. 13 | func success() -> bool: 14 | return _error == OK and _result == HTTPRequest.RESULT_SUCCESS 15 | 16 | ## Checks whether the [member status] is between 200 and 299 (inclusive), see [url]https://developer.mozilla.org/en-US/docs/Web/HTTP/Status[/url]. 17 | func status_ok() -> bool: 18 | return status >= 200 and status < 300 19 | 20 | ## Checks whether the [member status] is between 400 and 599 (inclusive), see [url]https://developer.mozilla.org/en-US/docs/Web/HTTP/Status[/url]. 21 | func status_err() -> bool: 22 | return status >= 400 and status < 600 23 | 24 | ## The response body as a [String].[br] 25 | ## For other formatting (ascii, utf16, ...) or special use-cases (file I/O, ...), it is possible to access the raw body's [member bytes].[br] 26 | ## You should cache this return value instead of calling the funciton multiple times. 27 | func body_as_string() -> String: 28 | return bytes.get_string_from_utf8() 29 | 30 | ## Attempt to parse the response [member bytes] into a [Dictionary] or [Array], returns null on failure.[br][br] 31 | ## It is possible to cast the return type to a [Dictionary] with "[code]as Dictionary[/code]" to receive autocomplete and other benefits when the parsing was successful.[br] 32 | ## If you want error handling for the JSON deserialization, make an instance of [JSON] and call [method JSON.parse] on it, passing in the return value of [method HTTPResult.body_as_string]. This allows the usage of [method JSON.get_error_message] and [method JSON.get_error_line] to get potential error information.[br][br] 33 | ## [b]Note:[/b] Godot always converts JSON numbers to [float]s! 34 | func body_as_json() -> Variant: 35 | return JSON.parse_string(body_as_string()) 36 | 37 | # Constructs a new [HTTPResult] from an [enum @GlobalScope.Error] code. (Used internally, hidden from API list) 38 | static func _from_error(err: Error) -> HTTPResult: 39 | var h := HTTPResult.new() 40 | h._error = err 41 | return h 42 | 43 | # Constructs a new [HTTPResult] from the return value of [signal HTTPRequest.request_completed]. (Used internally, hidden from API list) 44 | @warning_ignore("unsafe_cast") 45 | static func _from_array(a: Array) -> HTTPResult: 46 | var h := HTTPResult.new() 47 | h._result = a[0] as HTTPRequest.Result 48 | h.status = a[1] as int 49 | h.headers = _headers_to_dict(a[2] as PackedStringArray) 50 | h.bytes = a[3] as PackedByteArray 51 | return h 52 | 53 | # Converts a [PackedStringArray] of headers into a [Dictionary]. The header names will be in lowercase, as some web servers prefer this approach and them being case-insensitive as per specification. Therefore, it is good practice to not rely on capitalization. (Used internally, hidden from API list) 54 | static func _headers_to_dict(headers_arr: PackedStringArray) -> Dictionary: 55 | var dict := {} 56 | for h in headers_arr: 57 | var split := h.split(":", true, 1) 58 | dict[split[0].to_lower()] = split[1].strip_edges() 59 | 60 | return dict 61 | --------------------------------------------------------------------------------