Files
nakama-cpp/impl/httpCurl/NHttpClientLibCurl.cpp
Andrés Felipe Méndez 4eb339156b fix crash add test, add worker thread (#192)
* fix crash add test, add worker thread

* Improve HTTP/WS cleanup and test timing

Resolves cleanup/race issues and speeds up integration tests:

- impl/httpCurl/NHttpClientLibCurl.cpp: Remove curl handle earlier and deduplicate calls to curl_multi_remove_handle(), logging errors immediately if removal fails to avoid double-remove races.
- impl/wsWslay/NWebsocketWslay.cpp: Avoid heap allocation for received message by capturing a moved stack NBytes in the callback; ensure destructor always stops IO, joins the thread and cleans up the connection to prevent lingering IO.
- integrationtests/src/NTest.cpp: Run deleteAccountAsync calls concurrently and pump only the HTTP client while waiting to avoid RT client races; consume results best-effort. Also reduce tick/check timeouts from 50ms to 5ms to speed test loops.

* pr comments

* Add exception destructors and Android test updates

Provide out-of-line destructor definitions for NException and NRtException and declare their destructors in headers to satisfy the ABI and avoid inline dtor issues.

Revamp Android integration test setup: add gradle.properties and .gitignore entries, switch build.gradle to use prebuilt native libraries staged under jniLibs (avoid Gradle/CMake integration conflicts), add usesCleartextTraffic to AndroidManifest, and update integration main to run tests with client/rt client factories and server params. The deploy.sh script was rewritten to support building native libs via CMake presets, staging .so and libc++_shared, assembling the APK, installing to a device/emulator, setting up adb reverse, streaming logcat and parsing test results. Also increase tournament test durations to 60s to be more tolerant of slow environments (e.g. emulators).

* Refactor test cleanup, logging and Taskfile

Multiple changes to improve test reliability and logging behavior:

- integrationtests/src/NTest.cpp: Use a detached cleanup thread to pump the HTTP client with a 10s timeout, wait/join or detach based on completion, and avoid hangs when tick() blocks. Added <future> and best-effort future get handling; emit warning if cleanup times out.
- impl/httpLibHttpClient/NHttpClientLibHC.cpp and impl/wsLibHttpClient/NWebsocketLibHC.cpp: Map NLogLevel::Debug to HCTraceLevel::Warning (was Verbose) to adjust trace level mapping.
- Taskfile.yml: Only apply Xcode code-signing overrides on macOS, add docker compose --wait to service startup in multiple tasks, make the test task start the server automatically and copy built .dlls into the integrationtests folder (best-effort) so tests can find native binaries.
- interface headers: add <stdexcept> includes in NException.h and realtime/rtdata/NRtException.h to ensure exception types are available.

These changes aim to prevent test hangs during shutdown, ensure dependent services are ready before tests run, and fix missing includes and logging-level mapping.

* Add cancellation and retry integration tests

Introduce integrationtests/src/test_cancellation.cpp with a suite of tests covering request cancellation, disconnect recovery, and retry/idempotency scenarios (cancel_noPendingRequests, cancel_inflightRequests, cancel_recoveryAfterDisconnect, retry_afterConnectionError, retry_afterUnauthenticated, retry_afterSessionExpiry, retry_storageIdempotent, retry_authAndGetAccount). Also update integrationtests/src/NTestLib.cpp to declare and run test_cancellation so the new tests are included in the test run. These tests improve coverage around client disconnect behavior, CancelledByUser handling, and recovery/retry patterns.

* Add configure status; recreate XTaskQueue

NHttpClientLibHC.cpp: ensure m_queue_terminated is reset before terminating, then recreate the XTaskQueue after XTaskQueueTerminate so the client can be reused after cancellation; log an error if queue creation fails.

* Make Android deploy script cross-platform

Taskfile: loosened tests-android invocation (DEVICE is optional, ABI default kept) and delegate build/deploy to integrationtests/android/deploy.sh with an --abi flag.

deploy.sh: added OS detection and portability fixes (Windows/macOS/Linux), auto-detect ANDROID_HOME (including LOCALAPPDATA on Windows) and normalize backslashes, prefer adb from PATH with fallback to ANDROID_HOME, auto-detect/suggest JDK 17 and set JAVA_HOME if found, and normalize ANDROID_NDK_HOME. Build now runs from repo root (pushd/popd) and uses HOST_OS for NDK STL path. All adb calls now go through a resolved ADB variable. These changes improve robustness and cross-platform support for Android integration tests.

* linux build

* inline deploy.sh

* rename

* run android tests windows

* dis own proces on powershell

* ensure exported interfaces have out-of-line key functions avoiding linker RTTI issues

* revert to private wslay

* change to 10 ms
2026-03-05 13:51:41 -05:00

319 lines
9.9 KiB
C++

#include "NHttpClientLibCurl.h"
#include "nakama-cpp/log/NLogger.h"
#include <curl/curl.h>
#include <memory.h>
#include <nakama-cpp/NHttpTransportInterface.h>
#include <nakama-cpp/log/NLogger.h>
#include <string>
#if __ANDROID__
#include "AndroidCA.h"
#endif
static int debug_callback(CURL* handle, curl_infotype type, char* data, size_t size, void* userp) {
const char* text;
(void)handle; /* prevent compiler warning */
(void)userp;
switch (type) {
case CURLINFO_TEXT:
NLOG(Nakama::NLogLevel::Debug, "libcurl debug info <=> Text: %s", data);
default: /* in case a new one is introduced */
return 0;
case CURLINFO_HEADER_OUT:
text = "=> Send header";
break;
case CURLINFO_DATA_OUT:
text = "=> Send data";
break;
case CURLINFO_SSL_DATA_OUT:
text = "=> Send SSL data";
break;
case CURLINFO_HEADER_IN:
text = "<= Recv header";
break;
case CURLINFO_DATA_IN:
text = "<= Recv data";
break;
case CURLINFO_SSL_DATA_IN:
text = "<= Recv SSL data";
break;
}
NLOG(Nakama::NLogLevel::Debug, "libcurl debug info: %s", text);
return 0;
}
static size_t write_callback(char* buffer, size_t size, size_t nmemb, void* user_ctx) {
Nakama::NHttpClientLibCurlContext* curl_ctx = (Nakama::NHttpClientLibCurlContext*)user_ctx;
if (buffer != NULL) {
// char buffer from libcurl is not null terminated.
curl_ctx->append_body(std::string(buffer, nmemb * size));
}
return nmemb * size;
}
namespace Nakama {
NHttpClientLibCurl::NHttpClientLibCurl(const NPlatformParameters& platformParameters)
: _curl_multi(curl_multi_init(), curl_multi_cleanup) {}
void NHttpClientLibCurl::request(const NHttpRequest& req, const NHttpResponseCallback& callback) noexcept {
std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl_easy(curl_easy_init(), curl_easy_cleanup);
std::string uri = _base_uri + req.path + (req.queryArgs.empty() ? "" : "?");
for (auto p : req.queryArgs) {
uri += p.first + "=" + p.second + "&";
};
curl_slist* headers_list = NULL;
for (auto it = req.headers.begin(); it != req.headers.end(); it++) {
headers_list = curl_slist_append(headers_list, (it->first + ": " + it->second).c_str());
if (headers_list == NULL) {
NLOG(Nakama::NLogLevel::Error, "error writing header: %s", it->first.c_str());
return;
}
}
const char* callMethod = nullptr;
switch (req.method) {
case NHttpReqMethod::POST:
callMethod = "POST";
// manually set content-length if there's no body.
if (req.body.empty()) {
headers_list = curl_slist_append(headers_list, "Content-Length: 0");
if (headers_list == NULL) {
NLOG(Nakama::NLogLevel::Error, "error writing header: Content-Length");
return;
}
}
break;
case NHttpReqMethod::GET:
callMethod = "GET";
break;
case NHttpReqMethod::PUT:
callMethod = "PUT";
break;
case NHttpReqMethod::DEL:
callMethod = "DELETE";
break;
}
CURLcode curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_URL, uri.c_str());
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("setting url", curl_code, callback);
return;
}
if (_timeout >= std::chrono::milliseconds(0)) {
curl_code = curl_easy_setopt(
curl_easy.get(), CURLOPT_TIMEOUT_MS, std::chrono::duration_cast<std::chrono::milliseconds>(_timeout).count());
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("setting timeout", curl_code, callback);
return;
}
}
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_HTTPHEADER, headers_list);
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("adding headers", curl_code, callback);
return;
}
if (req.body != "") {
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_COPYPOSTFIELDS, req.body.c_str());
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("copying post fields", curl_code, callback);
return;
}
}
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_CUSTOMREQUEST, callMethod);
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("adding call method", curl_code, callback);
return;
}
#if __ANDROID__
CACertificateData* data = Nakama::getCaCertificates();
if (data == nullptr) {
// Has System.loadLibrary("nakama-sdk") been called?
NLOG(Nakama::NLogLevel::Error, "error obtaining CA Certificates.");
return;
} else {
struct curl_blob blob;
blob.data = reinterpret_cast<char*>(data->data);
blob.len = data->len;
blob.flags = CURL_BLOB_COPY;
curl_easy_setopt(curl_easy.get(), CURLOPT_CAINFO_BLOB, &blob);
}
#endif
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_WRITEFUNCTION, write_callback);
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("adding write function", curl_code, callback);
return;
}
std::unique_ptr<NHttpClientLibCurlContext> curl_ctx(new NHttpClientLibCurlContext(callback, headers_list));
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_WRITEDATA, curl_ctx.get());
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("adding curl context", curl_code, callback);
return;
}
/* ask libcurl to show us the verbose output */
#ifdef CURL_DEBUG
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_VERBOSE, 1L);
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("setting verbose output", curl_code, callback);
return;
}
curl_code = curl_easy_setopt(curl_easy.get(), CURLOPT_DEBUGFUNCTION, debug_callback);
if (curl_code != CURLE_OK) {
handle_curl_easy_set_opt_error("adding debug function", curl_code, callback);
return;
}
#endif
const std::lock_guard lock(_mutex);
CURLMcode curl_multi_code = curl_multi_add_handle(_curl_multi.get(), curl_easy.get());
if (curl_multi_code != CURLM_OK) {
auto response = std::shared_ptr<NHttpResponse>(new NHttpResponse());
response->errorMessage =
"curl_multi_add_handle() error while adding handle, code: " + std::string(curl_multi_strerror(curl_multi_code));
response->statusCode = -1;
callback(response);
return;
};
std::pair<std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>, std::unique_ptr<NHttpClientLibCurlContext>> pair(
std::move(curl_easy), std::move(curl_ctx));
_contexts.emplace_back(std::move(pair));
}
void NHttpClientLibCurl::setBaseUri(const std::string& uri) { _base_uri = uri; }
void NHttpClientLibCurl::setTimeout(std::chrono::milliseconds timeout) { _timeout = timeout; }
void NHttpClientLibCurl::tick() {
std::unique_lock lock(_mutex, std::try_to_lock);
if (!lock.owns_lock()) {
return; // return immediately, tick() is expected to get called again. no reason to make tick block.
}
int running_handles = 0;
CURLMcode mc = curl_multi_perform(_curl_multi.get(), &running_handles);
lock.unlock();
if (mc) {
// log but do not return -- still see if any pending messages
NLOG(Nakama::NLogLevel::Error, "curl_multi_perform() failed, code %d.\n", (int)mc);
}
struct CURLMsg* m = nullptr;
CURLcode result;
do {
int msgq = 0;
lock.lock();
m = curl_multi_info_read(_curl_multi.get(), &msgq);
lock.unlock();
if (m && (m->msg == CURLMSG_DONE)) {
lock.lock();
result = m->data.result; // cache here because when we remove the easy handle, m is invalidated.
CURL* e = m->easy_handle;
long response_code;
CURLcode curl_code = curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &response_code);
std::unique_ptr<NHttpClientLibCurlContext> context = NULL;
auto it = _contexts.begin();
while (it != _contexts.end()) {
if (it->first.get() == e) {
break;
}
it++;
}
if (it == _contexts.end()) {
NLOG(Nakama::NLogLevel::Error, "curl_multi_info_read() error: untracked easy handle.");
continue;
}
context = std::move(it->second);
mc = curl_multi_remove_handle(_curl_multi.get(), e);
if (mc) {
NLOG(Nakama::NLogLevel::Error, "curl_multi_remove_handle() failed, code %d.\n", (int)mc);
}
_contexts.remove(*it);
lock.unlock();
auto callback = context->get_callback();
if (callback != NULL) {
NLOG_DEBUG("invoking curl callback");
auto response = std::shared_ptr<NHttpResponse>(new NHttpResponse());
response->body = context->get_body();
if (result != CURLE_OK) {
NLOG(Nakama::NLogLevel::Error, "curl easy handle returned code: %d \n", (int)result);
response->statusCode = InternalStatusCodes::CONNECTION_ERROR;
} else {
response->statusCode = static_cast<int>(response_code);
if (curl_code != CURLE_OK) {
NLOG(
Nakama::NLogLevel::Error, "curl_easy_getinfo() failed when getting response code, code %d.\n",
(int)curl_code);
}
}
callback(response);
}
}
} while (m);
}
void NHttpClientLibCurl::cancelAllRequests() {
const std::lock_guard lock(_mutex);
for (auto it = _contexts.begin(); it != _contexts.end(); it++) {
NHttpResponsePtr responsePtr(new NHttpResponse());
responsePtr->statusCode = InternalStatusCodes::CANCELLED_BY_USER;
responsePtr->errorMessage = "cancelled by user";
it->second->get_callback()(responsePtr);
CURLMcode mc = curl_multi_remove_handle(_curl_multi.get(), it->first.get());
if (mc != CURLM_OK) {
NLOG(Nakama::NLogLevel::Error, "curl_multi_remove_handle() failed, code %d.\n", (int)mc);
}
}
_contexts.clear();
}
void NHttpClientLibCurl::handle_curl_easy_set_opt_error(
std::string action,
CURLcode code,
const NHttpResponseCallback& callback) {
auto response = std::shared_ptr<NHttpResponse>(new NHttpResponse());
response->errorMessage =
"curl_easy_getinfo() error while : " + action + ", code: " + std::string(curl_easy_strerror(code));
response->statusCode = -1;
callback(response);
}
}; // namespace Nakama