208 lines
6 KiB
GDScript
208 lines
6 KiB
GDScript
@tool
|
|
extends Node
|
|
|
|
# An adapter which implements the HTTP protocol.
|
|
class_name NakamaHTTPAdapter
|
|
|
|
# The logger to use with the adapter.
|
|
var logger : RefCounted = NakamaLogger.new()
|
|
|
|
# The timeout for requests
|
|
var timeout : int = 3
|
|
# If request should be automatically retried when a network error occurs.
|
|
var auto_retry : bool = true
|
|
# The maximum number of time a request will be retried when auto_retry is true
|
|
var auto_retry_count : int = 3
|
|
var auto_retry_backoff_base : int = 10
|
|
# Whether or not to use threads when making HTTP requests.
|
|
var use_threads : bool = true
|
|
|
|
var _pending = {}
|
|
var id : int = 0
|
|
|
|
class AsyncRequest:
|
|
var id : int
|
|
var request : HTTPRequest
|
|
var uri : String
|
|
var method : int
|
|
var headers : PackedStringArray
|
|
var body : PackedByteArray
|
|
var retry_count := 3
|
|
var backoff_time := 10
|
|
var logger : NakamaLogger
|
|
|
|
var cancelled = false
|
|
var result : int = HTTPRequest.RESULT_NO_RESPONSE
|
|
var response_code : int = -1
|
|
var response_body : PackedByteArray
|
|
var timer : SceneTreeTimer = null
|
|
var cur_try : int = 1
|
|
var rng = RandomNumberGenerator.new()
|
|
|
|
func _init(p_id : int, p_request : HTTPRequest, p_uri : String,
|
|
p_method : int, p_headers : PackedStringArray, p_body : PackedByteArray,
|
|
p_retry_count : int, p_backoff_time : int, p_logger : NakamaLogger):
|
|
rng.seed = Time.get_ticks_usec()
|
|
id = p_id
|
|
request = p_request
|
|
uri = p_uri
|
|
method = p_method
|
|
headers = p_headers
|
|
body = p_body
|
|
retry_count = p_retry_count
|
|
backoff_time = p_backoff_time
|
|
logger = p_logger
|
|
|
|
func should_retry():
|
|
return cur_try < retry_count and not cancelled
|
|
|
|
func retry():
|
|
var time = pow(backoff_time, cur_try) * rng.randf_range(0.5, 1)
|
|
logger.debug("Retrying request %d. Tries left: %d. Backoff: %d ms" % [
|
|
id, retry_count - cur_try, time
|
|
])
|
|
cur_try += 1
|
|
await backoff(time)
|
|
if cancelled:
|
|
return
|
|
return await make_request()
|
|
|
|
func make_request():
|
|
var err = request.request(uri, headers, method, body.get_string_from_utf8())
|
|
if err != OK:
|
|
await request.get_tree().process_frame
|
|
result = HTTPRequest.RESULT_CANT_CONNECT
|
|
logger.debug("Request %d failed to start, error: %d" % [id, err])
|
|
return
|
|
|
|
var args = await request.request_completed
|
|
result = args[0]
|
|
response_code = args[1]
|
|
response_body = args[3]
|
|
|
|
func backoff(p_time : int):
|
|
timer = request.get_tree().create_timer(p_time / 1000)
|
|
await timer.timeout
|
|
timer = null
|
|
|
|
func cancel():
|
|
cancelled = true
|
|
request.cancel_request()
|
|
if timer:
|
|
timer.time_left = 0
|
|
else:
|
|
request.call_deferred("emit_signal", "request_completed", HTTPRequest.RESULT_REQUEST_FAILED, 0, [], [])
|
|
|
|
func parse_result():
|
|
if cancelled:
|
|
return NakamaException.new("Request cancelled", -1, -1, true)
|
|
elif result != HTTPRequest.RESULT_SUCCESS:
|
|
if result == null:
|
|
result = 0
|
|
return NakamaException.new("HTTPRequest failed!", result)
|
|
|
|
var json = JSON.new()
|
|
|
|
var json_error = json.parse(response_body.get_string_from_utf8())
|
|
if json_error != OK:
|
|
logger.debug("Unable to parse request %d response. JSON error: %d, response code: %d" % [
|
|
id, json.error, response_code
|
|
])
|
|
return NakamaException.new("Failed to decode JSON response", response_code)
|
|
|
|
var parsed = json.get_data()
|
|
|
|
if response_code != HTTPClient.RESPONSE_OK:
|
|
var error = ""
|
|
var code = -1
|
|
if typeof(parsed) == TYPE_DICTIONARY:
|
|
if "message" in parsed:
|
|
error = parsed["message"]
|
|
elif "error" in parsed:
|
|
error = parsed["error"]
|
|
else:
|
|
error = str(parsed)
|
|
code = parsed["code"] if "code" in parsed else -1
|
|
else:
|
|
error = str(parsed)
|
|
if typeof(error) == TYPE_DICTIONARY:
|
|
error = JSON.stringify(error)
|
|
logger.debug("Request %d returned response code: %d, RPC code: %d, error: %s" % [
|
|
id, response_code, code, error
|
|
])
|
|
return NakamaException.new(error, response_code, code)
|
|
|
|
return parsed
|
|
|
|
|
|
# Send a HTTP request.
|
|
# @param method - HTTP method to use for this request.
|
|
# @param uri - The fully qualified URI to use.
|
|
# @param headers - Request headers to set.
|
|
# @param body - Request content body to set.
|
|
# @param timeoutSec - Request timeout.
|
|
# Returns a task which resolves to the contents of the response.
|
|
func send_async(p_method : String, p_uri : String, p_headers : Dictionary, p_body : PackedByteArray):
|
|
var req = HTTPRequest.new()
|
|
req.timeout = timeout
|
|
if use_threads and OS.get_name() != 'Web':
|
|
req.use_threads = true # Threads not available nor needed on the web.
|
|
|
|
# Parse method
|
|
var method = HTTPClient.METHOD_GET
|
|
if p_method == "POST":
|
|
method = HTTPClient.METHOD_POST
|
|
elif p_method == "PUT":
|
|
method = HTTPClient.METHOD_PUT
|
|
elif p_method == "DELETE":
|
|
method = HTTPClient.METHOD_DELETE
|
|
elif p_method == "HEAD":
|
|
method = HTTPClient.METHOD_HEAD
|
|
var headers = PackedStringArray()
|
|
|
|
# Parse headers
|
|
headers.append("Accept: application/json")
|
|
for k in p_headers:
|
|
headers.append("%s: %s" % [k, p_headers[k]])
|
|
|
|
id += 1
|
|
var retry = auto_retry_count if auto_retry else 0
|
|
var backoff = auto_retry_backoff_base
|
|
_pending[id] = AsyncRequest.new(id, req, p_uri, method, headers, p_body, retry, backoff, logger)
|
|
|
|
logger.debug("Sending request [ID: %d, Method: %s, Uri: %s, Headers: %s, Body: %s, Timeout: %d, Retries: %d, Backoff base: %d ms]" % [
|
|
id, p_method, p_uri, p_headers, p_body.get_string_from_utf8(), timeout, retry, backoff
|
|
])
|
|
|
|
add_child(req)
|
|
|
|
return await _send_async(id, _pending)
|
|
|
|
func get_last_token():
|
|
return id
|
|
|
|
func cancel_request(p_token):
|
|
if _pending.has(p_token):
|
|
_pending[p_token].cancel()
|
|
|
|
static func _clear_request(p_request : AsyncRequest, p_pending : Dictionary, p_id : int):
|
|
if not p_request.request.is_queued_for_deletion():
|
|
p_request.logger.debug("Freeing request %d" % p_id)
|
|
p_request.request.queue_free()
|
|
p_pending.erase(p_id)
|
|
|
|
static func _send_async(p_id : int, p_pending : Dictionary):
|
|
|
|
var req : AsyncRequest = p_pending[p_id]
|
|
await req.make_request()
|
|
|
|
while req.result != HTTPRequest.RESULT_SUCCESS:
|
|
req.logger.debug("Request %d failed with result: %d, response code: %d" % [
|
|
p_id, req.result, req.response_code
|
|
])
|
|
if not req.should_retry():
|
|
break
|
|
await req.retry()
|
|
|
|
_clear_request(req, p_pending, p_id)
|
|
return req.parse_result()
|