WIP checkpoint: Architecture: ApiClient autoload (HTTP + JWT + error contract)
This commit is contained in:
@@ -9,9 +9,12 @@ config_version=5
|
|||||||
config/name="AGP-Game"
|
config/name="AGP-Game"
|
||||||
run/main_scene="res://scenes/main_menu.tscn"
|
run/main_scene="res://scenes/main_menu.tscn"
|
||||||
config/features=PackedStringArray("4.6.2")
|
config/features=PackedStringArray("4.6.2")
|
||||||
|
agp/api_base_url="http://127.0.0.1:8000"
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
|
ApiClient="*res://scripts/api_client.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
window/size/viewport_width=1920
|
window/size/viewport_width=1920
|
||||||
|
|||||||
+136
-216
@@ -1,32 +1,33 @@
|
|||||||
class_name ApiClient
|
class_name ApiClient
|
||||||
extends Node
|
extends Node
|
||||||
## @group Networking
|
## Central HTTP client singleton (autoload name: "ApiClient").
|
||||||
## @description Central HTTP client singleton that wraps Godot's HTTPRequest node.
|
|
||||||
##
|
##
|
||||||
## Every network call flows through this class. Callers use the high-level
|
## Every network call in the game flows through this class. Callers use the
|
||||||
## helpers (login, register, get_worlds, etc.) or the generic request()
|
## high-level helpers ([method login], [method register], [method get_worlds],
|
||||||
## primitive. All results conform to the [result-dictionary] contract below.
|
## [method health_check], [method get_me]) or the generic [method request]
|
||||||
|
## primitive. All results conform to the result-dictionary contract below.
|
||||||
##
|
##
|
||||||
## ## Result Dictionary Contract
|
## [b]Result Dictionary Contract[/b]
|
||||||
## Every public method returns a Dictionary with this shape:
|
## Every public method returns (after `await`) a Dictionary with this shape:
|
||||||
##
|
##
|
||||||
## {
|
## {
|
||||||
## "ok": bool, # true for HTTP 2xx, false otherwise
|
## "ok": bool, # true for HTTP 2xx, false otherwise
|
||||||
## "status": int, # HTTP status code (0 on network failure)
|
## "status": int, # HTTP status code (0 on network failure)
|
||||||
## "data": Variant, # parsed JSON body on success
|
## "data": Variant, # parsed JSON body on success (null otherwise)
|
||||||
## "detail": String, # human-readable error message
|
## "detail": String, # human-readable message
|
||||||
## }
|
## }
|
||||||
##
|
##
|
||||||
## Network failures (timeout, DNS, refused connection) return status=0 with
|
## Network failures (timeout / DNS / refused connection) return `status == 0`
|
||||||
## a detail describing the failure. Non-2xx responses return the actual status
|
## with a `detail` describing the transport failure. Non-2xx responses return
|
||||||
## code and the backend's "detail" field from the JSON error body.
|
## the real HTTP status and the backend's `detail` field from the JSON error
|
||||||
|
## body when present. No caller can crash on a failed request.
|
||||||
##
|
##
|
||||||
## ## Token Storage
|
## [b]Token storage[/b]
|
||||||
## JWT bearer tokens are kept in memory. The [method set_token] method is
|
## JWTs are kept in memory. [method set_token] is the single seam for future
|
||||||
## the single seam for token persistence — override it to save to disk.
|
## disk persistence (override it to save to `user://`).
|
||||||
##
|
##
|
||||||
## ## Usage
|
## [b]Usage[/b]
|
||||||
## var result = await ApiClient.login("alice", "secret")
|
## var result: Dictionary = await ApiClient.login("alice", "secret")
|
||||||
## if result.ok:
|
## if result.ok:
|
||||||
## print("Logged in!")
|
## print("Logged in!")
|
||||||
## else:
|
## else:
|
||||||
@@ -39,76 +40,62 @@ extends Node
|
|||||||
var _access_token: String = ""
|
var _access_token: String = ""
|
||||||
var _token_type: String = "Bearer"
|
var _token_type: String = "Bearer"
|
||||||
|
|
||||||
## Current bearer token string (e.g. "Bearer eyJ...").
|
## Full Authorization header value, e.g. "Bearer eyJ...". Empty if no token.
|
||||||
## Read-only — use [method set_token] to change.
|
|
||||||
var bearer_token: String:
|
var bearer_token: String:
|
||||||
get:
|
get:
|
||||||
return _token_type + " " + _access_token if _access_token else ""
|
return _token_type + " " + _access_token if _access_token != "" else ""
|
||||||
|
|
||||||
## Whether a token has been set.
|
|
||||||
|
## Whether an access token is currently stored.
|
||||||
func has_token() -> bool:
|
func has_token() -> bool:
|
||||||
return _access_token != ""
|
return _access_token != ""
|
||||||
|
|
||||||
## Set the access token (called after login / token refresh).
|
|
||||||
## @param access_token The raw JWT string from the server.
|
## Set the access token (called after login / register / token refresh).
|
||||||
## @param token_type Token type, defaults to "Bearer".
|
## This is the persistence seam: override to also write to disk.
|
||||||
func set_token(access_token: String, token_type: String = "Bearer") -> void:
|
func set_token(access_token: String, token_type: String = "Bearer") -> void:
|
||||||
_access_token = access_token
|
_access_token = access_token
|
||||||
_token_type = token_type
|
_token_type = token_type
|
||||||
|
|
||||||
|
|
||||||
## Clear stored tokens.
|
## Clear stored tokens.
|
||||||
func clear_token() -> void:
|
func clear_token() -> void:
|
||||||
_access_token = ""
|
_access_token = ""
|
||||||
_token_type = "Bearer"
|
_token_type = "Bearer"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Internal HTTPRequest node
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## The internal HTTPRequest node. Created lazily on first use.
|
|
||||||
var _http_request: HTTPRequest
|
|
||||||
|
|
||||||
## Base URL read from project settings.
|
|
||||||
var _base_url: String
|
|
||||||
|
|
||||||
# Signal emitted when a request completes. The argument is the result dict.
|
|
||||||
signal request_completed(result: Dictionary)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Lifecycle
|
# Configuration
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var _base_url: String = "http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
## Request timeout in seconds. 0 disables the timeout.
|
||||||
|
var timeout_seconds: float = 10.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# Read base URL from project settings, fall back to localhost:8000.
|
_base_url = str(ProjectSettings.get_setting(
|
||||||
_base_url = ProjectSettings.get_setting(
|
|
||||||
"application/agp/api_base_url",
|
"application/agp/api_base_url",
|
||||||
"http://127.0.0.1:8000"
|
"http://127.0.0.1:8000"
|
||||||
)
|
)).rstrip("/")
|
||||||
# Ensure the setting exists so it appears in the editor.
|
|
||||||
if not ProjectSettings.has_setting("application/agp/api_base_url"):
|
|
||||||
ProjectSettings.set_setting("application/agp/api_base_url", _base_url)
|
|
||||||
ProjectSettings.save()
|
|
||||||
|
|
||||||
func _enter_tree() -> void:
|
|
||||||
_http_request = HTTPRequest.new()
|
|
||||||
_http_request.timeout = 10.0
|
|
||||||
_http_request.max_redirects = 8
|
|
||||||
_http_request.accept_gzip = true
|
|
||||||
add_child(_http_request)
|
|
||||||
_http_request.request_completed.connect(_on_http_request_completed)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Core request primitive
|
# Core request primitive
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Generic request method.
|
## Generic request primitive. Awaitable via `await`.
|
||||||
##
|
##
|
||||||
## @param method HTTPClient.Method enum value (GET, POST, PUT, DELETE).
|
## Uses a dedicated HTTPRequest node per call so concurrent requests can never
|
||||||
## @param path URL path (e.g. "/api/health").
|
## cross-talk, and so the result maps 1:1 to the awaiting caller.
|
||||||
## @param body Request body as String (JSON or form-encoded).
|
##
|
||||||
|
## @param method HTTPClient.Method enum value (GET/POST/PUT/PATCH/DELETE).
|
||||||
|
## @param path URL path, e.g. "/api/health".
|
||||||
|
## @param body Request body string (JSON or form-encoded).
|
||||||
## @param content_type "application/json" or "application/x-www-form-urlencoded".
|
## @param content_type "application/json" or "application/x-www-form-urlencoded".
|
||||||
## @param require_auth If true, attach bearer token header.
|
## @param require_auth If true, attach the stored bearer token when present.
|
||||||
## @return A result dictionary conforming to the contract.
|
## @return Result dictionary conforming to the contract.
|
||||||
func request(
|
func request(
|
||||||
method: HTTPClient.Method,
|
method: HTTPClient.Method,
|
||||||
path: String,
|
path: String,
|
||||||
@@ -116,224 +103,150 @@ func request(
|
|||||||
content_type: String = "application/json",
|
content_type: String = "application/json",
|
||||||
require_auth: bool = false
|
require_auth: bool = false
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var url: String = _base_url + path
|
var http := HTTPRequest.new()
|
||||||
|
http.timeout = timeout_seconds
|
||||||
|
http.max_redirects = 8
|
||||||
|
http.accept_gzip = true
|
||||||
|
add_child(http)
|
||||||
|
|
||||||
# Build headers.
|
var headers := PackedStringArray()
|
||||||
var headers: PackedStringArray = PackedStringArray()
|
headers.append("Accept: application/json")
|
||||||
|
if content_type != "":
|
||||||
headers.append("Content-Type: " + content_type)
|
headers.append("Content-Type: " + content_type)
|
||||||
if require_auth and has_token():
|
if require_auth and has_token():
|
||||||
headers.append("Authorization: " + bearer_token)
|
headers.append("Authorization: " + bearer_token)
|
||||||
|
|
||||||
# Fire the request.
|
var url := _base_url + path
|
||||||
var err: int = _http_request.request(url, headers, method, body)
|
var err := http.request(url, headers, method, body)
|
||||||
if err != OK:
|
if err != OK:
|
||||||
return _network_error(err, "Failed to start request: " + _error_string(err))
|
http.queue_free()
|
||||||
|
return _make_result(false, 0, null, "Failed to start request: " + _error_string(err))
|
||||||
|
|
||||||
# Wait for completion.
|
# request_completed emits (result, response_code, headers, body).
|
||||||
var result_data = await request_completed
|
var args: Array = await http.request_completed
|
||||||
return result_data
|
var result_code: int = args[0]
|
||||||
|
var response_code: int = args[1]
|
||||||
|
var response_body: PackedByteArray = args[3]
|
||||||
|
http.queue_free()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# Transport-level failure (timeout / DNS / refused / TLS, etc.).
|
||||||
# Signal handler
|
if result_code != HTTPRequest.RESULT_SUCCESS:
|
||||||
# ---------------------------------------------------------------------------
|
return _make_result(false, 0, null, _transport_error_detail(result_code))
|
||||||
|
|
||||||
func _on_http_request_completed(
|
|
||||||
result: int,
|
|
||||||
response_code: int,
|
|
||||||
headers: PackedStringArray,
|
|
||||||
body: PackedByteArray
|
|
||||||
) -> void:
|
|
||||||
# result == HTTPRequest.RESULT_SUCCESS means the transport succeeded.
|
|
||||||
# response_code == 0 means no response was received (e.g. timeout).
|
|
||||||
if result != HTTPRequest.RESULT_SUCCESS:
|
|
||||||
var detail: String = _network_error_detail(result)
|
|
||||||
var dict: Dictionary = {
|
|
||||||
"ok": false,
|
|
||||||
"status": 0,
|
|
||||||
"data": null,
|
|
||||||
"detail": detail,
|
|
||||||
}
|
|
||||||
request_completed.emit(dict)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
# Server accepted the connection but returned no HTTP status line.
|
||||||
if response_code == 0:
|
if response_code == 0:
|
||||||
var dict: Dictionary = {
|
return _make_result(false, 0, null, "No response received from server.")
|
||||||
"ok": false,
|
|
||||||
"status": 0,
|
|
||||||
"data": null,
|
|
||||||
"detail": "No response received from server.",
|
|
||||||
}
|
|
||||||
request_completed.emit(dict)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Parse JSON body.
|
var json_string := response_body.get_string_from_utf8()
|
||||||
var json_string: String = body.get_string_from_utf8()
|
var ok := response_code >= 200 and response_code < 300
|
||||||
var data = null
|
|
||||||
var detail: String = ""
|
|
||||||
|
|
||||||
if response_code >= 200 and response_code < 300:
|
if ok:
|
||||||
# Success — try to parse JSON (works for both objects and arrays).
|
var data: Variant = _parse_json(json_string)
|
||||||
var parse_result = JSON.parse_string(json_string)
|
return _make_result(true, response_code, data, "OK")
|
||||||
if parse_result is Dictionary or parse_result is Array:
|
|
||||||
data = parse_result
|
# Non-2xx: surface the backend's "detail" message when available.
|
||||||
# Extract backend detail if present (e.g. {"ok": true, "detail": "OK"}).
|
var detail := "HTTP error %d" % response_code
|
||||||
if data is Dictionary and data.has("detail"):
|
var parsed: Variant = _parse_json(json_string)
|
||||||
detail = str(data["detail"])
|
if parsed is Dictionary and (parsed as Dictionary).has("detail"):
|
||||||
else:
|
detail = str((parsed as Dictionary)["detail"])
|
||||||
detail = "OK"
|
return _make_result(false, response_code, null, detail)
|
||||||
else:
|
|
||||||
# Not valid JSON — return raw string.
|
|
||||||
data = json_string
|
|
||||||
detail = "OK"
|
|
||||||
else:
|
|
||||||
# Error response — try to extract detail from JSON.
|
|
||||||
var parse_result = JSON.parse_string(json_string)
|
|
||||||
if parse_result is Dictionary:
|
|
||||||
detail = str(parse_result.get("detail", "HTTP error %d" % response_code))
|
|
||||||
else:
|
|
||||||
detail = "HTTP error %d" % response_code
|
|
||||||
|
|
||||||
var dict: Dictionary = {
|
|
||||||
"ok": response_code >= 200 and response_code < 300,
|
|
||||||
"status": response_code,
|
|
||||||
"data": data,
|
|
||||||
"detail": detail,
|
|
||||||
}
|
|
||||||
request_completed.emit(dict)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# High-level helpers
|
# High-level helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Login with username and password.
|
## Log in with username/password (OAuth2PasswordRequestForm).
|
||||||
##
|
## On success stores the returned access token.
|
||||||
## Sends a POST to /api/auth/login with form-encoded credentials
|
|
||||||
## (OAuth2PasswordRequestForm format). On success, stores the token.
|
|
||||||
##
|
|
||||||
## @param username The user's username.
|
|
||||||
## @param password The user's password.
|
|
||||||
## @return Result dictionary.
|
|
||||||
func login(username: String, password: String) -> Dictionary:
|
func login(username: String, password: String) -> Dictionary:
|
||||||
var form_data: String = "username=" + username.uri_encode() + "&password=" + password.uri_encode()
|
var form := "username=" + username.uri_encode() + "&password=" + password.uri_encode()
|
||||||
var result = await request(
|
var result := await request(
|
||||||
HTTPClient.METHOD_POST,
|
HTTPClient.METHOD_POST,
|
||||||
"/api/auth/login",
|
"/api/auth/login",
|
||||||
form_data,
|
form,
|
||||||
"application/x-www-form-urlencoded",
|
"application/x-www-form-urlencoded",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
if result.ok and result.data is Dictionary:
|
_store_token_from_result(result)
|
||||||
var access_token = result.data.get("access_token", "")
|
|
||||||
if access_token != "":
|
|
||||||
set_token(access_token)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Register a new user.
|
|
||||||
##
|
## Register a new user. On success stores the returned access token.
|
||||||
## Sends a POST to /api/auth/register with JSON body.
|
## @param date_of_birth ISO date string "YYYY-MM-DD".
|
||||||
## On success, stores the returned token.
|
|
||||||
##
|
|
||||||
## @param username The desired username.
|
|
||||||
## @param email The user's email address.
|
|
||||||
## @param password The desired password.
|
|
||||||
## @param date_of_birth Date of birth in YYYY-MM-DD format.
|
|
||||||
## @return Result dictionary.
|
|
||||||
func register(
|
func register(
|
||||||
username: String,
|
username: String,
|
||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
date_of_birth: String
|
date_of_birth: String
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
var payload: Dictionary = {
|
var payload := {
|
||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
"password": password,
|
"password": password,
|
||||||
"date_of_birth": date_of_birth,
|
"date_of_birth": date_of_birth,
|
||||||
}
|
}
|
||||||
var json_body: String = JSON.stringify(payload)
|
var result := await request(
|
||||||
var result = await request(
|
|
||||||
HTTPClient.METHOD_POST,
|
HTTPClient.METHOD_POST,
|
||||||
"/api/auth/register",
|
"/api/auth/register",
|
||||||
json_body,
|
JSON.stringify(payload),
|
||||||
"application/json",
|
"application/json",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
if result.ok and result.data is Dictionary:
|
_store_token_from_result(result)
|
||||||
var access_token = result.data.get("access_token", "")
|
|
||||||
if access_token != "":
|
|
||||||
set_token(access_token)
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## List worlds visible to the current user.
|
|
||||||
##
|
## List worlds visible to the current user (requires auth).
|
||||||
## Sends a GET to /api/worlds/ with the bearer token.
|
## @param visibility Optional filter: "public", "private", "friends_only".
|
||||||
##
|
func get_worlds(skip: int = 0, limit: int = 50, visibility: String = "") -> Dictionary:
|
||||||
## @param skip Number of worlds to skip (pagination offset).
|
var path := "/api/worlds/?skip=%d&limit=%d" % [skip, limit]
|
||||||
## @param limit Maximum number of worlds to return.
|
|
||||||
## @param visibility Optional filter: "public", "private", or "friends_only".
|
|
||||||
## @return Result dictionary.
|
|
||||||
func get_worlds(
|
|
||||||
skip: int = 0,
|
|
||||||
limit: int = 50,
|
|
||||||
visibility: String = ""
|
|
||||||
) -> Dictionary:
|
|
||||||
var path: String = "/api/worlds/?skip=%d&limit=%d" % [skip, limit]
|
|
||||||
if visibility != "":
|
if visibility != "":
|
||||||
path += "&visibility=" + visibility.uri_encode()
|
path += "&visibility=" + visibility.uri_encode()
|
||||||
return await request(
|
return await request(HTTPClient.METHOD_GET, path, "", "application/json", true)
|
||||||
HTTPClient.METHOD_GET,
|
|
||||||
path,
|
|
||||||
"",
|
|
||||||
"application/json",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
## Check the server health endpoint.
|
|
||||||
##
|
## Ping the backend health endpoint (no auth).
|
||||||
## @return Result dictionary.
|
|
||||||
func health_check() -> Dictionary:
|
func health_check() -> Dictionary:
|
||||||
return await request(
|
return await request(HTTPClient.METHOD_GET, "/api/health", "", "application/json", false)
|
||||||
HTTPClient.METHOD_GET,
|
|
||||||
"/api/health",
|
|
||||||
"",
|
|
||||||
"application/json",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
## Get current user profile.
|
|
||||||
##
|
## Fetch the current user's profile from the stored JWT (requires auth).
|
||||||
## @return Result dictionary.
|
|
||||||
func get_me() -> Dictionary:
|
func get_me() -> Dictionary:
|
||||||
return await request(
|
return await request(HTTPClient.METHOD_GET, "/api/users/me", "", "application/json", true)
|
||||||
HTTPClient.METHOD_GET,
|
|
||||||
"/api/users/me",
|
|
||||||
"",
|
|
||||||
"application/json",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Persistence seam
|
# Internals
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Called before the node is freed. Override to persist tokens to disk.
|
func _store_token_from_result(result: Dictionary) -> void:
|
||||||
func _exit_tree() -> void:
|
if result.ok and result.data is Dictionary:
|
||||||
# TODO: Save tokens to disk (e.g. user://tokens.cfg).
|
var token: String = str((result.data as Dictionary).get("access_token", ""))
|
||||||
pass
|
if token != "":
|
||||||
|
set_token(token)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func _network_error(err_code: int, fallback: String) -> Dictionary:
|
func _parse_json(text: String) -> Variant:
|
||||||
|
if text == "":
|
||||||
|
return null
|
||||||
|
var parsed: Variant = JSON.parse_string(text)
|
||||||
|
if parsed == null:
|
||||||
|
# null is ambiguous (valid JSON "null" vs parse failure). Fall back to
|
||||||
|
# the raw string so callers never lose the body entirely.
|
||||||
|
return text if text != "null" else null
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
func _make_result(ok: bool, status: int, data: Variant, detail: String) -> Dictionary:
|
||||||
return {
|
return {
|
||||||
"ok": false,
|
"ok": ok,
|
||||||
"status": 0,
|
"status": status,
|
||||||
"data": null,
|
"data": data,
|
||||||
"detail": fallback,
|
"detail": detail,
|
||||||
}
|
}
|
||||||
|
|
||||||
func _network_error_detail(result_code: int) -> String:
|
|
||||||
|
func _transport_error_detail(result_code: int) -> String:
|
||||||
match result_code:
|
match result_code:
|
||||||
HTTPRequest.RESULT_CANT_CONNECT:
|
HTTPRequest.RESULT_CANT_CONNECT:
|
||||||
return "Could not connect to server. Is it running?"
|
return "Could not connect to server. Is it running?"
|
||||||
@@ -345,11 +258,18 @@ func _network_error_detail(result_code: int) -> String:
|
|||||||
return "TLS handshake failed."
|
return "TLS handshake failed."
|
||||||
HTTPRequest.RESULT_NO_RESPONSE:
|
HTTPRequest.RESULT_NO_RESPONSE:
|
||||||
return "No response from server."
|
return "No response from server."
|
||||||
|
HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED:
|
||||||
|
return "Response body too large."
|
||||||
|
HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED:
|
||||||
|
return "Failed to decompress response body."
|
||||||
|
HTTPRequest.RESULT_REQUEST_FAILED:
|
||||||
|
return "Request failed."
|
||||||
HTTPRequest.RESULT_TIMEOUT:
|
HTTPRequest.RESULT_TIMEOUT:
|
||||||
return "Request timed out."
|
return "Request timed out."
|
||||||
_:
|
_:
|
||||||
return "Network error (code %d)." % result_code
|
return "Network error (code %d)." % result_code
|
||||||
|
|
||||||
|
|
||||||
func _error_string(err_code: int) -> String:
|
func _error_string(err_code: int) -> String:
|
||||||
match err_code:
|
match err_code:
|
||||||
OK:
|
OK:
|
||||||
|
|||||||
Reference in New Issue
Block a user