// retoor <retoor@molodetz.nl>
#include "http_client.h"
#include <curl/curl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define HTTP_MAX_RETRIES 3
#define HTTP_RETRY_DELAY_MS 2000
struct http_client_t {
char *bearer_token;
long timeout_seconds;
long connect_timeout_seconds;
bool show_spinner;
};
struct response_buffer_t {
char *data;
size_t size;
};
static struct timespec spinner_start_time = {0, 0};
static volatile int spinner_running = 0;
static double get_elapsed_seconds(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (now.tv_sec - spinner_start_time.tv_sec) +
(now.tv_nsec - spinner_start_time.tv_nsec) / 1e9;
}
static void *spinner_thread(void *arg) {
(void)arg;
const char *frames[] = {"", "", "", "", "", "", "", "", "", ""};
int frame = 0;
while (spinner_running) {
double elapsed = get_elapsed_seconds();
fprintf(stderr, "\r%s Querying AI... (%.1fs) ", frames[frame % 10], elapsed);
fflush(stderr);
frame++;
usleep(80000);
}
return NULL;
}
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
size_t total_size = size * nmemb;
struct response_buffer_t *response = (struct response_buffer_t *)userp;
if (total_size > SIZE_MAX - response->size - 1) {
return 0;
}
char *ptr = realloc(response->data, response->size + total_size + 1);
if (!ptr) {
return 0;
}
response->data = ptr;
memcpy(&(response->data[response->size]), contents, total_size);
response->size += total_size;
response->data[response->size] = '\0';
return total_size;
}
http_client_handle http_client_create(const char *bearer_token) {
struct http_client_t *client = calloc(1, sizeof(struct http_client_t));
if (!client) return NULL;
if (bearer_token) {
client->bearer_token = strdup(bearer_token);
if (!client->bearer_token) {
free(client);
return NULL;
}
}
client->timeout_seconds = 300;
client->connect_timeout_seconds = 10;
client->show_spinner = true;
return client;
}
void http_client_destroy(http_client_handle client) {
if (!client) return;
free(client->bearer_token);
free(client);
}
void http_client_set_show_spinner(http_client_handle client, bool show) {
if (client) client->show_spinner = show;
}
void http_client_set_timeout(http_client_handle client, long timeout_seconds) {
if (client) client->timeout_seconds = timeout_seconds;
}
void http_client_set_connect_timeout(http_client_handle client, long timeout_seconds) {
if (client) client->connect_timeout_seconds = timeout_seconds;
}
r_status_t http_post(http_client_handle client, const char *url,
const char *data, char **response) {
if (!client || !url || !response) return R_ERROR_INVALID_ARG;
CURL *curl = NULL;
struct curl_slist *headers = NULL;
struct response_buffer_t resp = {NULL, 0};
int retry_count = 0;
pthread_t spinner_tid = 0;
r_status_t status = R_SUCCESS;
*response = NULL;
bool actually_show_spinner = client->show_spinner && isatty(STDERR_FILENO);
if (actually_show_spinner) {
clock_gettime(CLOCK_MONOTONIC, &spinner_start_time);
spinner_running = 1;
pthread_create(&spinner_tid, NULL, spinner_thread, NULL);
}
while (retry_count < HTTP_MAX_RETRIES) {
free(resp.data);
resp.data = malloc(1);
resp.size = 0;
if (!resp.data) {
status = R_ERROR_OUT_OF_MEMORY;
goto cleanup;
}
resp.data[0] = '\0';
curl = curl_easy_init();
if (!curl) {
status = R_ERROR_HTTP_CONNECTION;
goto cleanup;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, client->connect_timeout_seconds);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, client->timeout_seconds);
headers = curl_slist_append(headers, "Content-Type: application/json");
if (client->bearer_token) {
char bearer_header[2048];
snprintf(bearer_header, sizeof(bearer_header), "Authorization: Bearer %s",
client->bearer_token);
headers = curl_slist_append(headers, bearer_header);
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&resp);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
headers = NULL;
curl_easy_cleanup(curl);
curl = NULL;
if (res == CURLE_OK) {
*response = resp.data;
resp.data = NULL;
status = R_SUCCESS;
goto cleanup;
}
retry_count++;
if (actually_show_spinner) {
spinner_running = 0;
pthread_join(spinner_tid, NULL);
spinner_tid = 0;
fprintf(stderr, "\r \r");
}
fprintf(stderr, "Network error: %s (attempt %d/%d)\n",
curl_easy_strerror(res), retry_count, HTTP_MAX_RETRIES);
if (retry_count < HTTP_MAX_RETRIES) {
fprintf(stderr, "Retrying in %d seconds...\n", HTTP_RETRY_DELAY_MS / 1000);
usleep(HTTP_RETRY_DELAY_MS * 1000);
if (actually_show_spinner) {
clock_gettime(CLOCK_MONOTONIC, &spinner_start_time);
spinner_running = 1;
pthread_create(&spinner_tid, NULL, spinner_thread, NULL);
}
}
}
status = R_ERROR_HTTP_TIMEOUT;
cleanup:
if (actually_show_spinner && spinner_tid) {
spinner_running = 0;
pthread_join(spinner_tid, NULL);
fprintf(stderr, "\r \r");
fflush(stderr);
}
if (headers) curl_slist_free_all(headers);
if (curl) curl_easy_cleanup(curl);
free(resp.data);
return status;
}
r_status_t http_get(http_client_handle client, const char *url, char **response) {
if (!client || !url || !response) return R_ERROR_INVALID_ARG;
CURL *curl = NULL;
struct curl_slist *headers = NULL;
struct response_buffer_t resp = {NULL, 0};
int retry_count = 0;
r_status_t status = R_SUCCESS;
*response = NULL;
while (retry_count < HTTP_MAX_RETRIES) {
free(resp.data);
resp.data = malloc(1);
resp.size = 0;
if (!resp.data) {
status = R_ERROR_OUT_OF_MEMORY;
goto cleanup;
}
resp.data[0] = '\0';
curl = curl_easy_init();
if (!curl) {
status = R_ERROR_HTTP_CONNECTION;
goto cleanup;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, client->connect_timeout_seconds);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
headers = curl_slist_append(headers, "Content-Type: application/json");
if (client->bearer_token) {
char bearer_header[2048];
snprintf(bearer_header, sizeof(bearer_header), "Authorization: Bearer %s",
client->bearer_token);
headers = curl_slist_append(headers, bearer_header);
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&resp);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
headers = NULL;
curl_easy_cleanup(curl);
curl = NULL;
if (res == CURLE_OK) {
*response = resp.data;
resp.data = NULL;
status = R_SUCCESS;
goto cleanup;
}
retry_count++;
fprintf(stderr, "Network error: %s (attempt %d/%d)\n",
curl_easy_strerror(res), retry_count, HTTP_MAX_RETRIES);
if (retry_count < HTTP_MAX_RETRIES) {
fprintf(stderr, "Retrying in %d seconds...\n", HTTP_RETRY_DELAY_MS / 1000);
usleep(HTTP_RETRY_DELAY_MS * 1000);
}
}
status = R_ERROR_HTTP_TIMEOUT;
cleanup:
if (headers) curl_slist_free_all(headers);
if (curl) curl_easy_cleanup(curl);
free(resp.data);
return status;
}
r_status_t http_post_simple(const char *url, const char *bearer_token,
const char *data, char **response) {
http_client_handle client = http_client_create(bearer_token);
if (!client) return R_ERROR_OUT_OF_MEMORY;
r_status_t status = http_post(client, url, data, response);
http_client_destroy(client);
return status;
}
r_status_t http_get_simple(const char *url, const char *bearer_token, char **response) {
http_client_handle client = http_client_create(bearer_token);
if (!client) return R_ERROR_OUT_OF_MEMORY;
http_client_set_show_spinner(client, false);
r_status_t status = http_get(client, url, response);
http_client_destroy(client);
return status;
}