From 666b55ca774c41b31e8b853dced56487b7595e5a Mon Sep 17 00:00:00 2001 From: Max Hilbrunner Date: Tue, 18 Apr 2023 06:21:27 +0200 Subject: [PATCH] Update HTTP docs for Godot 4 --- tutorials/networking/http_request_class.rst | 171 +++++++++++-------- tutorials/networking/img/rest_api_scene.png | Bin 6084 -> 0 bytes tutorials/networking/img/rest_api_scene.webp | Bin 0 -> 3208 bytes 3 files changed, 97 insertions(+), 74 deletions(-) delete mode 100644 tutorials/networking/img/rest_api_scene.png create mode 100644 tutorials/networking/img/rest_api_scene.webp diff --git a/tutorials/networking/http_request_class.rst b/tutorials/networking/http_request_class.rst index a66edadcd..7a999ec9b 100644 --- a/tutorials/networking/http_request_class.rst +++ b/tutorials/networking/http_request_class.rst @@ -1,70 +1,91 @@ -:article_outdated: True - .. _doc_http_request_class: Making HTTP requests ==================== -The :ref:`HTTPRequest ` node is the easiest way to make HTTP requests in Godot. -It is backed by the more low-level :ref:`HTTPClient `, for which a tutorial is available :ref:`here `. +Why use HTTP? +------------- -For the sake of this example, we will create a simple UI with a button, that when pressed will start the HTTP request to the specified URL. +`HTTP requests `_ are useful +to communicate with web servers and other non-Godot programs. + +Compared to Godot's other networking features (like +:ref:`High-level multiplayer `), +HTTP requests have more overhead and take more time to get going, +so they aren't suited for real-time communication, and aren't great to send +lots of small updates as is common for multiplayer gameplay. + +HTTP, however, offers interoperability with external +web resources and is great at sending and receiving large amounts +of data, for example to transfer files like game assets. + +So HTTP may be useful for your game's login system, lobby browser, +to retrieve some information from the web or to download game assets. + +This tutorial assumes some familiarity with Godot and the Godot Editor. +Refer to the :ref:`Introduction ` and the +:ref:`Step by step ` tutorial, especially its +:ref:`Nodes and Scenes ` and +:ref:`Creating your first script ` pages if needed. + +HTTP requests in Godot +---------------------- + +The :ref:`HTTPRequest ` node is the easiest way to make HTTP requests in Godot. +It is backed by the more low-level :ref:`HTTPClient `, +for which a tutorial is available :ref:`here `. + +For this example, we will make an HTTP request to GitHub to retrieve the name +of the latest Godot release. .. warning:: - When exporting to Android, make sure to enable the ``INTERNET`` + When exporting to **Android**, make sure to enable the **Internet** permission in the Android export preset before exporting the project or using one-click deploy. Otherwise, network communication of any kind will be - blocked by Android. + blocked by the Android OS. -Preparing scene ---------------- +Preparing the scene +------------------- -Create a new empty scene, add a CanvasLayer as the root node and add a script to it. Then add two child nodes to it: a Button and an HTTPRequest node. You will need to connect the following signals to the CanvasLayer script: +Create a new empty scene, add a root :ref:`Node ` and add a script to it. +Then add a :ref:`HTTPRequest ` node as a child. -- Button.pressed: When the button is pressed, we will start the request. -- HTTPRequest.request_completed: When the request is completed, we will get the requested data as an argument. +.. image:: img/rest_api_scene.webp -.. image:: img/rest_api_scene.png +Scripting the request +--------------------- -Scripting ---------- - -Below is all the code we need to make it work. The URL points to an online API mocker; it returns a predefined JSON string, which we will then parse to get access to the data. +When the project is started (so in ``_ready()``), we're going to send an HTTP request +to Github using our :ref:`HTTPRequest ` node, +and once the request completes, we're going to parse the returned JSON data, +look for the ``name`` field and print that to console. .. tabs:: .. code-tab:: gdscript GDScript - extends CanvasLayer + extends Node func _ready(): - $HTTPRequest.connect("request_completed", self, "_on_request_completed") - $Button.connect("pressed", self, "_on_Button_pressed") - - func _on_Button_pressed(): - $HTTPRequest.request("http://www.mocky.io/v2/5185415ba171ea3a00704eed") + $HTTPRequest.request_completed.connect(_on_request_completed) + $HTTPRequest.request("https://api.github.com/repos/godotengine/godot/releases/latest") func _on_request_completed(result, response_code, headers, body): - var json = JSON.parse(body.get_string_from_utf8()) - print(json.result) + var json = JSON.parse_string(body.get_string_from_utf8()) + print(json["name"]) .. code-tab:: csharp using Godot; - - public partial class MyCanvasLayer : CanvasLayer + + public partial class MyNode : Node { public override void _Ready() { GetNode("HTTPRequest").RequestCompleted += OnRequestCompleted; - GetNode("Button").Pressed += OnButtonPressed; - } - - private void OnButtonPressed() - { HttpRequest httpRequest = GetNode("HTTPRequest"); - httpRequest.Request("http://www.mocky.io/v2/5185415ba171ea3a00704eed"); + httpRequest.Request("https://api.github.com/repos/godotengine/godot/releases/latest"); } private void OnRequestCompleted(long result, long responseCode, string[] headers, byte[] body) @@ -74,58 +95,60 @@ Below is all the code we need to make it work. The URL points to an online API m } } -With this, you should see ``(hello:world)`` printed on the console; ``hello`` being a key, ``world`` being a value, and both of them are of type :ref:`String `. - +Save the script and the scene, and run the project. +The name of the most recent Godot release on Github should be printed to the output log. For more information on parsing JSON, see the class references for :ref:`JSON `. -Note that you may want to check whether the ``result`` equals ``RESULT_SUCCESS`` and whether a JSON parsing error occurred, see the JSON class reference and :ref:`HTTPRequest ` for more. +Note that you may want to check whether the ``result`` equals ``RESULT_SUCCESS`` +and whether a JSON parsing error occurred, see the JSON class reference and +:ref:`HTTPRequest ` for more. -Of course, you can also set custom HTTP headers. These are given as a string array, with each string containing a header in the format ``"header: value"``. -For example, to set a custom user agent (the HTTP ``user-agent`` header) you could use the following: +You have to wait for a request to finish before sending another one. +Making multiple request at once requires you to have one node per request. +A common strategy is to create and delete HTTPRequest nodes at runtime as necessary. + +Sending data to the server +-------------------------- + +Until now, we have limited ourselves to requesting data from a server. +But what if you need to send data to the server? Here is a common way of doing it: .. tabs:: .. code-tab:: gdscript GDScript - $HTTPRequest.request("http://www.mocky.io/v2/5185415ba171ea3a00704eed", ["user-agent: YourCustomUserAgent"]) + var json = JSON.stringify(data_to_send) + var headers = ["Content-Type: application/json"] + $HTTPRequest.request(url, headers, HTTPClient.METHOD_POST, json) + + .. code-tab:: csharp + + string json = Json.Stringify(dataToSend); + string[] headers = new string[] { "Content-Type: application/json" }; + HttpRequest httpRequest = GetNode("HTTPRequest"); + httpRequest.Request(url, headers, HttpClient.Method.Post, json); + +Setting custom HTTP headers +--------------------------- + +Of course, you can also set custom HTTP headers. These are given as a string array, +with each string containing a header in the format ``"header: value"``. +For example, to set a custom user agent (the HTTP ``User-Agent`` header) you could use the following: + +.. tabs:: + + .. code-tab:: gdscript GDScript + + $HTTPRequest.request("https://api.github.com/repos/godotengine/godot/releases/latest", ["User-Agent: YourCustomUserAgent"]) .. code-tab:: csharp HttpRequest httpRequest = GetNode("HTTPRequest"); - httpRequest.Request("http://www.mocky.io/v2/5185415ba171ea3a00704eed", new string[] { "user-agent: YourCustomUserAgent" }); + httpRequest.Request("https://api.github.com/repos/godotengine/godot/releases/latest", new string[] { "User-Agent: YourCustomUserAgent" }); -Please note that, for SSL/TLS encryption and thus HTTPS URLs to work, you may need to take some steps as described :ref:`here `. +.. warning:: -Also, when calling APIs using authorization, be aware that someone might analyse and decompile your released application and thus may gain access to any embedded authorization information like tokens, usernames or passwords. -That means it is usually not a good idea to embed things such as database access credentials inside your game. Avoid providing information useful to an attacker whenever possible. - -Sending data to server ----------------------- - -Until now, we have limited ourselves to requesting data from a server. But what if you need to send data to the server? Here is a common way of doing it: - -.. tabs:: - - .. code-tab:: gdscript GDScript - - func _make_post_request(url, data_to_send, use_ssl): - # Convert data to json string: - var query = JSON.stringify(data_to_send) - # Add 'Content-Type' header: - var headers = ["Content-Type: application/json"] - $HTTPRequest.request(url, headers, use_ssl, HTTPClient.METHOD_POST, query) - - .. code-tab:: csharp - - public void MakePostRequest(string url, Variant dataToSend, bool useSsl) - { - // Convert data to json string: - string query = Json.Stringify(dataToSend); - // Add 'Content-Type' header: - string[] headers = new string[] { "Content-Type: application/json" }; - HttpRequest httpRequest = GetNode("HTTPRequest"); - httpRequest.Request(url, headers, useSsl, HttpClient.Method.Post, query); - } - -Keep in mind that you have to wait for a request to finish before sending another one. Making multiple request at once requires you to have one node per request. -A common strategy is to create and delete HTTPRequest nodes at runtime as necessary. + Be aware that someone might analyse and decompile your released application and + thus may gain access to any embedded authorization information like tokens, usernames or passwords. + That means it is usually not a good idea to embed things such as database + access credentials inside your game. Avoid providing information useful to an attacker whenever possible. diff --git a/tutorials/networking/img/rest_api_scene.png b/tutorials/networking/img/rest_api_scene.png deleted file mode 100644 index bf53c99a426cb754cca16e364622af286a978c70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6084 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 diff --git a/tutorials/networking/img/rest_api_scene.webp b/tutorials/networking/img/rest_api_scene.webp new file mode 100644 index 0000000000000000000000000000000000000000..53eaeb44ba73326c58467e20a07675f7dfdc4d10 GIT binary patch literal 3208 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