@icon("./buffered-http-icon.svg") @tool extends Twitcher ## Http client that bufferes the requests and sends them sequentialy class_name BufferedHTTPClient ## Will be send when a new request was added to queue signal request_added(request: RequestData) ## Will be send when a request is done. signal request_done(response: ResponseData) ## Contains the request data to be send class RequestData extends RefCounted: ## The client that the request belongs too var client: BufferedHTTPClient ## The request node that is executing the request var http_request: HTTPRequest ## Path of the request var path: String ## The method that is used to call request var method: int ## The request headers var headers: Dictionary ## The body that is requested (TODO does it make more sense to make a Byte Array out of it?) var body: String = "" ## Amount of retries var retry: int ## When you are done free the request func queue_free() -> void: http_request.queue_free() ## Contains the response data class ResponseData extends RefCounted: ## Result of the request see `HTTPRequest.Result` var result: int ## Response code from the request like 200 for OK var response_code: int ## the initial request data var request_data: RequestData ## The body of the response as byte array var response_data: PackedByteArray ## The response header as dictionary, where multiple keys are concatenated with ';' var response_header: Dictionary ## Had the response an error var error: bool ## When you are done free the request func queue_free() -> void: request_data.queue_free() ## When a request fails max_error_count then cancel that request -1 for endless amount of tries. @export var max_error_count : int = -1 @export var custom_header : Dictionary[String, String] = { "Accept": "*/*" } var requests : Array[RequestData] = [] var current_request : RequestData var current_response_data : PackedByteArray = PackedByteArray() var responses : Dictionary = {} var error_count : int ## Only one poll at a time so block for all other tries to call it var polling: bool var processing: bool: get: return not requests.is_empty() || current_request != null ## Starts a request that will be handled as soon as the client gets free. ## Use HTTPClient.METHOD_* for the method. func request(path: String, method: int, headers: Dictionary, body: String) -> RequestData: logInfo("[%s] start request " % [ path ]) headers = headers.duplicate() headers.merge(custom_header) var req = RequestData.new() req.path = path req.method = method req.body = body req.headers = headers req.client = self req.http_request = HTTPRequest.new() req.http_request.use_threads = true req.http_request.timeout = 30 req.http_request.request_completed.connect(_on_request_completed.bind(req)) add_child(req.http_request) var err : Error = req.http_request.request(req.path, _pack_headers(req.headers), req.method, req.body) if err != OK: logError("Problems with request to %s cause of %s" % [path, error_string(err)]) requests.append(req) request_added.emit(req) logDebug("[%s] request started " % [ path ]) return req ## When the response is available return it otherwise wait for the response func wait_for_request(request_data: RequestData) -> ResponseData: if responses.has(request_data): var response = responses[request_data] responses.erase(request_data) request_data.queue_free() logDebug("response cached return directly from wait") return response var latest_response : ResponseData = null while (latest_response == null || request_data != latest_response.request_data): latest_response = await request_done logDebug("response received return from wait") responses.erase(request_data) request_data.queue_free() return latest_response func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, request_data: RequestData) -> void: var response_data : ResponseData = ResponseData.new() if result != HTTPRequest.Result.RESULT_SUCCESS: logInfo("[%s] problems with result \n\t> response code: %s \n\t> body: %s" % [request_data.path, response_code, body.get_string_from_utf8()]) response_data.error = true if result == HTTPRequest.Result.RESULT_CONNECTION_ERROR || result == HTTPRequest.Result.RESULT_TLS_HANDSHAKE_ERROR: if request_data.retry == max_error_count: printerr("Maximum amount of retries for the request. Abort request: %s" % [request_data.path]) return var wait_time = pow(2, request_data.retry) wait_time = min(wait_time, 30) logDebug("Error happend during connection. Wait for %s" % wait_time) await get_tree().create_timer(wait_time, true, false, true).timeout var http_request: HTTPRequest = request_data.http_request.duplicate() add_child(http_request) request_data.http_request = http_request request_data.retry += 1 http_request.request(request_data.path, _pack_headers(request_data.headers), request_data.method, request_data.body) http_request.request_completed.connect(_on_request_completed.bind(http_request)) response_data.result = result response_data.request_data = request_data response_data.response_data = body response_data.response_code = response_code response_data.response_header = _get_response_headers_as_dictionary(headers) responses[request_data] = response_data logInfo("[%s] request done with result HTTPRequest.Result[%s] " % [ request_data.path, result]) request_done.emit(response_data) func _get_response_headers_as_dictionary(headers: PackedStringArray) -> Dictionary: var header_dict: Dictionary = {} if headers == null: return header_dict for header in headers: var header_data = header.split(":", true, 1) var key = header_data[0] var val = header_data[1] if header_dict.has(key): header_dict[key] += "; " + val else: header_dict[key] = val return header_dict func _pack_headers(headers: Dictionary) -> PackedStringArray: var result: PackedStringArray = [] for header_key in headers: var header_value = headers[header_key] result.append("%s: %s" % [header_key, header_value]) return result ## The amount of requests that are pending func queued_request_size() -> int: var requests_size: int = requests.size() if current_request != null: requests_size += 1 return requests_size func empty_response(request_data: RequestData) -> ResponseData: var response_data = ResponseData.new() response_data.request_data = request_data response_data.response_data = [] response_data.response_code = 0 response_data.response_header = {} response_data.result = 0 return response_data # === LOGGER === static var logger: Dictionary = {} static func set_logger(error: Callable, info: Callable, debug: Callable) -> void: logger.debug = debug logger.info = info logger.error = error static func logDebug(text: String) -> void: if logger.has("debug"): logger.debug.call(text) static func logInfo(text: String) -> void: if logger.has("info"): logger.info.call(text) static func logError(text: String) -> void: if logger.has("error"): logger.error.call(text)