/* omhttp.c
 * This is an http output module based on omelasticsearch
 *
 * NOTE: read comments in module-template.h for more specifics!
 *
 * Supports profile-based configuration for common HTTP endpoints:
 * - profile="loki" for Grafana Loki
 * - profile="hec:splunk:event" for Splunk HTTP Event Collector (Endpoint EVENT)
 * - profile="hec:splunk:raw" for Splunk HTTP Event Collector (Endpoint RAW)
 * (proof-of-concept only, see
 * https://github.com/rsyslog/rsyslog/issues/5756 for feedback)
 * The profile configuration is based on this method :
 *      profile="techno:vendor:endpoint"
 *
 * Copyright 2011 Nathan Scott.
 * Copyright 2009-2018 Rainer Gerhards and Adiscon GmbH.
 * Copyright 2018 Christian Tramnitz
 *
 * This file is part of rsyslog.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *       -or-
 *       see COPYING.ASL20 in the source distribution
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "config.h"
#include "rsyslog.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>
#include <curl/curl.h>
#include <curl/easy.h>
#include <assert.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#if defined(__FreeBSD__)
    #include <unistd.h>
#endif
#include <json.h>
#include <zlib.h>
#include "conf.h"
#include "syslogd-types.h"
#include "srUtils.h"
#include "template.h"
#include "module-template.h"
#include "errmsg.h"
#include "cfsysline.h"
#include "unicode-helper.h"
#include "obj-types.h"
#include "ratelimit.h"
#include "ruleset.h"
#include "statsobj.h"

#ifndef O_LARGEFILE
    #define O_LARGEFILE 0
#endif

MODULE_TYPE_OUTPUT;
MODULE_TYPE_NOKEEP;
MODULE_CNFNAME("omhttp")

/* internal structures */
DEF_OMOD_STATIC_DATA;
DEFobjCurrIf(prop) DEFobjCurrIf(ruleset) DEFobjCurrIf(statsobj)


    typedef struct _targetStats {
    statsobj_t *defaultstats;

    STATSCOUNTER_DEF(ctrMessagesSubmitted, mutCtrMessagesSubmitted);  // Number of message submitted to module
    STATSCOUNTER_DEF(ctrMessagesSuccess, mutCtrMessagesSuccess);  // Number of messages successfully sent
    STATSCOUNTER_DEF(ctrMessagesFail, mutCtrMessagesFail);  // Number of messages that failed to send
    STATSCOUNTER_DEF(ctrMessagesRetry, mutCtrMessagesRetry);  // Number of messages requeued for retry
    STATSCOUNTER_DEF(ctrHttpRequestSuccess, mutCtrHttpRequestSuccess);  // Number of successful HTTP requests
    STATSCOUNTER_DEF(ctrHttpRequestFail, mutCtrHttpRequestFail);  // Number of failed HTTP req, 4XX+ are NOT failures
    STATSCOUNTER_DEF(ctrHttpStatusSuccess, mutCtrHttpStatusSuccess);  // Number of requests returning 1XX/2XX status
    STATSCOUNTER_DEF(ctrHttpStatusFail, mutCtrHttpStatusFail);  // Number of requests returning 300+ status
    STATSCOUNTER_DEF(ctrHttpRequestsCount, mutCtrHttpRequestsCount);  // Number of attempted HTTP requests
    STATSCOUNTER_DEF(httpRequestsBytes, mutHttpRequestsBytes);  // Number of bytes in HTTP requests
    STATSCOUNTER_DEF(httpRequestsTimeMs, mutHttpRequestsTimeMs);  // Number of Times(ms) in HTTP requests
    STATSCOUNTER_DEF(ctrHttpRequestsStatus0xx, mutCtrHttpRequestsStatus0xx);  // HTTP requests returning 0xx
    STATSCOUNTER_DEF(ctrHttpRequestsStatus1xx, mutCtrHttpRequestsStatus1xx);  // HTTP requests returning 1xx
    STATSCOUNTER_DEF(ctrHttpRequestsStatus2xx, mutCtrHttpRequestsStatus2xx);  // HTTP requests returning 2xx
    STATSCOUNTER_DEF(ctrHttpRequestsStatus3xx, mutCtrHttpRequestsStatus3xx);  // HTTP requests returning 3xx
    STATSCOUNTER_DEF(ctrHttpRequestsStatus4xx, mutCtrHttpRequestsStatus4xx);  // HTTP requests returning 4xx
    STATSCOUNTER_DEF(ctrHttpRequestsStatus5xx, mutCtrHttpRequestsStatus5xx);  // HTTP requests returning 5xx

} targetStats_t;


static prop_t *pInputName = NULL;
static int omhttpInstancesCnt = 0;


#define NAME_TPL_SPLK_EVENT " StdSplkEvent"
#define NAME_TPL_SPLK_RAW " StdSplkRAW"
static uchar template_StdSplkEvent[] = "\"{\\\"event\\\":\\\"%rawmsg:::json%\\\"}\"";
static uchar template_StdSplkRaw[] = "\"%rawmsg:::drop-last-lf%\n\"";

#define WRKR_DATA_TYPE_ES 0xBADF0001

#define HTTP_HEADER_CONTENT_JSON "Content-Type: application/json; charset=utf-8"
#define HTTP_HEADER_CONTENT_TEXT "Content-Type: text/plain"
#define HTTP_HEADER_CONTENT_KAFKA "Content-Type: application/vnd.kafka.v1+json"
#define HTTP_HEADER_ENCODING_GZIP "Content-Encoding: gzip"
#define HTTP_HEADER_EXPECT_EMPTY "Expect:"
#define HTTP_DEFAULT_HEALTHCHECKTIMEDELAY 30
#define HTTP_DEFAULT_FAILURE_RETRY_DELAY 1

#define VALID_BATCH_FORMATS "newline jsonarray kafkarest lokirest"

/* Splunk define value */
#define SPLUNK_HEC_RESTPATH_ENDPOINT_EVENT "services/collector/event"
#define SPLUNK_HEC_RESTPATH_ENDPOINT_RAW "services/collector/raw"
#define SPLUNK_HEC_RESTPATH_ENDPOINT_HEALTH "services/collector/health"
#define SPLUNK_HEC_HEADER_AUTH "Splunk "
#define SPLUNK_HEC_USER_AGENT "Rsyslog-omhttp"


/* Default batch size constants */
#define DEFAULT_MAX_BATCH_BYTES (10 * 1024 * 1024) /* 10 MB - default max message size for AWS API Gateway */
#define DEFAULT_REPLY_MAX_BYTES (1024 * 1024) /* 1 MB - default max response size */
#define SPLUNK_HEC_MAX_BATCH_BYTES (1024 * 1024) /* 1 MB - Splunk HEC recommended limit */
typedef enum batchFormat_e { FMT_NEWLINE, FMT_JSONARRAY, FMT_KAFKAREST, FMT_LOKIREST } batchFormat_t;
typedef enum vendor_e { LOKI, SPLUNK } vendor_t;

/* REST API uses this URL:
 * https://<hostName>:<restPort>/restPath
 */
typedef struct curl_slist HEADER;

typedef struct instanceConf_s {
    int defaultPort;
    int fdErrFile; /* error file fd or -1 if not open */
    pthread_mutex_t mutErrFile;
    uchar **serverBaseUrls;
    int numServers;
    long healthCheckTimeout;
    long restPathTimeout;
    uchar *token;
    uchar *uid;
    uchar *pwd;
    uchar *authBuf;
    uchar *httpcontenttype;
    uchar *headerContentTypeBuf;
    uchar *httpheaderkey;
    uchar *httpheadervalue;
    uchar *headerBuf;
    uchar **httpHeaders;
    int nHttpHeaders;
    uchar *restPath;
    uchar *checkPath;
    uchar *proxyHost;
    int proxyPort;
    uchar *tplName;
    uchar *errorFile;
    sbool batchMode;
    uchar *batchFormatName;
    batchFormat_t batchFormat;
    vendor_t vendor;
    sbool bFreeBatchFormatName;
    sbool dynRestPath;
    size_t maxBatchBytes;
    size_t maxBatchSize;
    size_t replyMaxBytes;
    sbool compress;
    int compressionLevel; /* Compression level for zlib, default=-1, fastest=1, best=9, none=0*/
    sbool useHttps;
    sbool allowUnsignedCerts;
    sbool skipVerifyHost;
    uchar *caCertFile;
    uchar *myCertFile;
    uchar *myPrivKeyFile;
    sbool reloadOnHup;
    sbool retryFailures;
    sbool retryAddMetadata;
    int nhttpRetryCodes;
    unsigned int *httpRetryCodes;
    int nIgnorableCodes;
    unsigned int *ignorableCodes;
    int ratelimitInterval;
    int ratelimitBurst;
    uchar *pszRatelimitName;
    /* for retries */
    ratelimit_t *ratelimiter;
    uchar *retryRulesetName;
    ruleset_t *retryRuleset;
    struct instanceConf_s *next;

    uchar *statsName;
    /* Stats Counter */
    targetStats_t *listObjStats;
    sbool statsBySenders;

    long healthCheckTimeDelay;  // Delay between two Health Check (in seconds)

    sbool *serverSuspended; /* Array: Is server N globally suspended? */
    time_t *serverNextRetry; /* Array: When can we try server N again? */
    time_t *serverLastCheck; /* Array: Last successful shared health state update */
    sbool *serverCheckInProgress; /* Array: Is a worker already probing server N? */
    pthread_mutex_t mutGlobalState; /* Protects these arrays */

} instanceData;

struct modConfData_s {
    rsconf_t *pConf; /* our overall config object */
    instanceConf_t *root, *tail;
};
static modConfData_t *loadModConf = NULL; /* modConf ptr to use for the current load process */

/*
 * Struct used to describe a server
 * Now each destination server have is own Curl Handle to improve TLS Handshake and HTTP Keep-Alive
 */
typedef struct serverData_s {
    CURL *curlCheckConnHandle; /* libcurl session handle for checking the server connection */
    CURL *curlPostHandle; /* libcurl session handle for posting data to the server */
    HEADER *curlHeader; /* json POST request info */
    HEADER *curlCheckHeader; /* health-check request headers */
    uchar *fullUrlPost; /* Keep the full url in cache if dynRestPath is off else last URL used */
    uchar *fullUrlHealth; /* Keep the full url healthCheck in cache */
    uchar *restPATH; /* Keep restPath last used */
    sbool gzipHeaderEnabled; /* Track whether the cached header includes gzip encoding */
} serverData_t;

typedef struct wrkrInstanceData {
    PTR_ASSERT_DEF
    instanceData *pData;
    int numServers;
    int serverIndex;
    size_t replyLen;
    size_t replyBufLen;
    char *reply;
    long httpStatusCode; /* http status code of response */
    sbool bzInitDone;
    z_stream zstrm; /* zip stream to use for gzip http compression */
    struct {
        uchar **data; /* array of strings, this will be batched up lazily */
        size_t sizeBytes; /* total length of this batch in bytes */
        size_t nmemb; /* number of messages in batch (for statistics counting) */

    } batch;
    struct {
        uchar *buf;
        size_t curLen;
        size_t len;
    } compressCtx;
    serverData_t **listServerDataWkr;

} wrkrInstanceData_t;

/* tables for interfacing with the v6 config system */
/* action (instance) parameters */
static struct cnfparamdescr actpdescr[] = {
    {"server", eCmdHdlrArray, 0},
    {"serverport", eCmdHdlrInt, 0},
    {"healthchecktimeout", eCmdHdlrInt, 0},
    {"restpathtimeout", eCmdHdlrInt, 0},
    {"httpcontenttype", eCmdHdlrGetWord, 0},
    {"httpheaderkey", eCmdHdlrGetWord, 0},
    {"httpheadervalue", eCmdHdlrString, 0},
    {"httpheaders", eCmdHdlrArray, 0},
    {"token", eCmdHdlrGetWord, 0},
    {"uid", eCmdHdlrGetWord, 0},
    {"pwd", eCmdHdlrGetWord, 0},
    {"restpath", eCmdHdlrGetWord, 0},
    {"checkpath", eCmdHdlrGetWord, 0},
    {"dynrestpath", eCmdHdlrBinary, 0},
    {"proxyhost", eCmdHdlrString, 0},
    {"proxyport", eCmdHdlrInt, 0},
    {"batch", eCmdHdlrBinary, 0},
    {"batch.format", eCmdHdlrGetWord, 0},
    {"batch.maxbytes", eCmdHdlrSize, 0},
    {"batch.maxsize", eCmdHdlrSize, 0},
    {"replymaxbytes", eCmdHdlrSize, 0},
    {"compress", eCmdHdlrBinary, 0},
    {"compress.level", eCmdHdlrInt, 0},
    {"usehttps", eCmdHdlrBinary, 0},
    {"errorfile", eCmdHdlrGetWord, 0},
    {"template", eCmdHdlrGetWord, 0},
    {"allowunsignedcerts", eCmdHdlrBinary, 0},
    {"skipverifyhost", eCmdHdlrBinary, 0},
    {"tls.cacert", eCmdHdlrString, 0},
    {"tls.mycert", eCmdHdlrString, 0},
    {"tls.myprivkey", eCmdHdlrString, 0},
    {"reloadonhup", eCmdHdlrBinary, 0},
    {"httpretrycodes", eCmdHdlrArray, 0},
    {"retry", eCmdHdlrBinary, 0},
    {"retry.addmetadata", eCmdHdlrBinary, 0},
    {"retry.ruleset", eCmdHdlrString, 0},
    {"ratelimit.interval", eCmdHdlrInt, 0},
    {"ratelimit.burst", eCmdHdlrInt, 0},
    {"ratelimit.name", eCmdHdlrString, 0},
    {"name", eCmdHdlrGetWord, 0},
    {"httpignorablecodes", eCmdHdlrArray, 0},
    {"profile", eCmdHdlrGetWord, 0},
    {"statsbysenders", eCmdHdlrBinary, 0},
    {"healthchecktimedelay", eCmdHdlrInt, 0},
};
static struct cnfparamblk actpblk = {CNFPARAMBLK_VERSION, sizeof(actpdescr) / sizeof(struct cnfparamdescr), actpdescr};

static rsRetVal curlSetup(wrkrInstanceData_t *pWrkrData);
static void curlCleanup(wrkrInstanceData_t *pWrkrData);
static void freeWorkerServerData(wrkrInstanceData_t *pWrkrData);
static void ATTR_NONNULL() curlCheckConnSetup(wrkrInstanceData_t *const pWrkrData, serverData_t *serverData);
static void cleanupServerData(serverData_t *serverData);
static void freeServerData(serverData_t *serverData);
static void ATTR_NONNULL(1) markServerSuspended(instanceData *pData, int serverIdx, time_t now);
static void ATTR_NONNULL(1) markServerFailed(instanceData *pData, int serverIdx, time_t now);
static void ATTR_NONNULL(1) markServerRateLimited(instanceData *pData, int serverIdx, time_t now);
static void ATTR_NONNULL(1) markServerAvailable(instanceData *pData, int serverIdx, time_t now);

/* compressCtx functions */
static void ATTR_NONNULL() initCompressCtx(wrkrInstanceData_t *pWrkrData);

static void ATTR_NONNULL() freeCompressCtx(wrkrInstanceData_t *pWrkrData);

static rsRetVal ATTR_NONNULL() resetCompressCtx(wrkrInstanceData_t *pWrkrData, size_t len);

static rsRetVal ATTR_NONNULL() growCompressCtx(wrkrInstanceData_t *pWrkrData, size_t newLen);

static rsRetVal ATTR_NONNULL() appendCompressCtx(wrkrInstanceData_t *pWrkrData, uchar *srcBuf, size_t srcLen);

BEGINcreateInstance
    CODESTARTcreateInstance;
    pData->fdErrFile = -1;
    pthread_mutex_init(&pData->mutErrFile, NULL);  // Mutex file
    pthread_mutex_init(&pData->mutGlobalState, NULL);  // Mutex for checkConn
    pData->caCertFile = NULL;
    pData->myCertFile = NULL;
    pData->myPrivKeyFile = NULL;
    pData->ratelimiter = NULL;
    pData->retryRulesetName = NULL;
    pData->retryRuleset = NULL;

    pData->serverSuspended = NULL;
    pData->serverNextRetry = NULL;
    pData->serverLastCheck = NULL;
    pData->serverCheckInProgress = NULL;
ENDcreateInstance

BEGINcreateWrkrInstance
    uchar **batchData;
    int i = 0;
    CODESTARTcreateWrkrInstance;
    PTR_ASSERT_SET_TYPE(pWrkrData, WRKR_DATA_TYPE_ES);
    pWrkrData->numServers = pData->numServers;
    pWrkrData->serverIndex = 0;
    pWrkrData->httpStatusCode = 0;
    pWrkrData->reply = NULL;
    pWrkrData->replyLen = 0;
    pWrkrData->replyBufLen = 0;
    pWrkrData->bzInitDone = 0;
    if (pData->batchMode) {
        pWrkrData->batch.nmemb = 0;
        pWrkrData->batch.sizeBytes = 0;
        batchData = (uchar **)malloc(pData->maxBatchSize * sizeof(uchar *));
        if (batchData == NULL) {
            LogError(0, RS_RET_OUT_OF_MEMORY,
                     "omhttp: cannot allocate memory for batch queue turning off batch mode\n");
            pData->batchMode = 0; /* at least it works */
        } else {
            pWrkrData->batch.data = batchData;
        }
    }
    pWrkrData->listServerDataWkr = calloc((size_t)pWrkrData->numServers, sizeof(serverData_t *));
    if (pWrkrData->listServerDataWkr == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: cannot allocate memory for listServer\n");
        ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
    }
    for (i = 0; i < pWrkrData->numServers; i++) {
        pWrkrData->listServerDataWkr[i] = (serverData_t *)calloc(1, sizeof(serverData_t));
        if (pWrkrData->listServerDataWkr[i] == NULL) {
            LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: cannot allocate memory for listServer : %d\n", i);
            ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
        }

        pWrkrData->listServerDataWkr[i]->curlCheckConnHandle = NULL;
        pWrkrData->listServerDataWkr[i]->curlPostHandle = NULL;
        pWrkrData->listServerDataWkr[i]->curlHeader = NULL;
        pWrkrData->listServerDataWkr[i]->curlCheckHeader = NULL;
        pWrkrData->listServerDataWkr[i]->fullUrlPost = NULL;
        pWrkrData->listServerDataWkr[i]->fullUrlHealth = NULL;
        pWrkrData->listServerDataWkr[i]->restPATH = NULL;
        pWrkrData->listServerDataWkr[i]->gzipHeaderEnabled = 0;
    }
    initCompressCtx(pWrkrData);
    iRet = curlSetup(pWrkrData);

finalize_it:
    if (iRet != RS_RET_OK) {
        freeWorkerServerData(pWrkrData);
        free(pWrkrData->listServerDataWkr);
        pWrkrData->listServerDataWkr = NULL;
    }
ENDcreateWrkrInstance

BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
    if (eFeat == sFEATURERepeatedMsgReduction) iRet = RS_RET_OK;
ENDisCompatibleWithFeature

BEGINfreeInstance
    int i;
    CODESTARTfreeInstance;
    if (pData->fdErrFile != -1) close(pData->fdErrFile);
    pthread_mutex_destroy(&pData->mutErrFile);
    for (i = 0; i < pData->numServers; ++i) free(pData->serverBaseUrls[i]);
    free(pData->serverBaseUrls);
    free(pData->token);
    free(pData->uid);
    free(pData->httpcontenttype);
    free(pData->headerContentTypeBuf);
    free(pData->httpheaderkey);
    free(pData->httpheadervalue);
    for (i = 0; i < pData->nHttpHeaders; ++i) {
        free((void *)pData->httpHeaders[i]);
    }
    free(pData->httpHeaders);
    pData->nHttpHeaders = 0;
    free(pData->pwd);
    free(pData->authBuf);
    free(pData->headerBuf);
    free(pData->restPath);
    free(pData->checkPath);
    free(pData->proxyHost);
    free(pData->tplName);
    free(pData->errorFile);
    free(pData->caCertFile);
    free(pData->myCertFile);
    free(pData->myPrivKeyFile);
    free(pData->httpRetryCodes);
    free(pData->retryRulesetName);
    free(pData->ignorableCodes);
    if (pData->ratelimiter != NULL) ratelimitDestruct(pData->ratelimiter);
    free(pData->pszRatelimitName);
    if (pData->bFreeBatchFormatName) free(pData->batchFormatName);
    if (pData->listObjStats != NULL) {
        const int numStats = pData->statsBySenders ? pData->numServers : 1;
        for (int j = 0; j < numStats; ++j) {
            if (pData->listObjStats[j].defaultstats != NULL) statsobj.Destruct(&(pData->listObjStats[j].defaultstats));
        }
        free(pData->listObjStats);
    }
    free(pData->statsName);

    free(pData->serverSuspended);
    free(pData->serverNextRetry);
    free(pData->serverLastCheck);
    free(pData->serverCheckInProgress);
    pthread_mutex_destroy(&pData->mutGlobalState);
ENDfreeInstance

BEGINfreeWrkrInstance
    CODESTARTfreeWrkrInstance;
    freeWorkerServerData(pWrkrData);
    free(pWrkrData->listServerDataWkr);
    pWrkrData->listServerDataWkr = NULL;

    free(pWrkrData->batch.data);
    pWrkrData->batch.data = NULL;

    if (pWrkrData->bzInitDone) deflateEnd(&pWrkrData->zstrm);
    freeCompressCtx(pWrkrData);
    free(pWrkrData->reply);
    pWrkrData->reply = NULL;
    pWrkrData->replyBufLen = 0;
    pWrkrData->replyLen = 0;

ENDfreeWrkrInstance

BEGINdbgPrintInstInfo
    int i;
    CODESTARTdbgPrintInstInfo;
    dbgprintf("omhttp\n");
    dbgprintf("\ttemplate='%s'\n", pData->tplName);
    dbgprintf("\tnumServers=%d\n", pData->numServers);
    dbgprintf("\thealthCheckTimeout=%lu\n", pData->healthCheckTimeout);
    dbgprintf("\thealthchecktimedelay=%ld\n", pData->healthCheckTimeDelay);
    dbgprintf("\trestPathTimeout=%lu\n", pData->restPathTimeout);
    dbgprintf("\tserverBaseUrls=");
    for (i = 0; i < pData->numServers; ++i) dbgprintf("%c'%s'", i == 0 ? '[' : ' ', pData->serverBaseUrls[i]);
    dbgprintf("]\n");
    dbgprintf("\tdefaultPort=%d\n", pData->defaultPort);
    dbgprintf("\ttoken='%s'\n", pData->token == NULL ? (uchar *)"(not configured)" : pData->token);
    dbgprintf("\tuid='%s'\n", pData->uid == NULL ? (uchar *)"(not configured)" : pData->uid);
    dbgprintf("\thttpcontenttype='%s'\n",
              pData->httpcontenttype == NULL ? (uchar *)"(not configured)" : pData->httpcontenttype);
    dbgprintf("\thttpheaderkey='%s'\n",
              pData->httpheaderkey == NULL ? (uchar *)"(not configured)" : pData->httpheaderkey);
    dbgprintf("\thttpheadervalue='%s'\n",
              pData->httpheadervalue == NULL ? (uchar *)"(not configured)" : pData->httpheadervalue);
    dbgprintf("\thttpHeaders=[");
    for (i = 0; i < pData->nHttpHeaders; ++i) dbgprintf("\t%s\n", pData->httpHeaders[i]);
    dbgprintf("\t]\n");
    dbgprintf("\tpwd=(%sconfigured)\n", pData->pwd == NULL ? "not " : "");
    dbgprintf("\trest path='%s'\n", pData->restPath);
    dbgprintf("\tcheck path='%s'\n", pData->checkPath);
    dbgprintf("\tdynamic rest path=%d\n", pData->dynRestPath);
    dbgprintf("\tproxy host='%s'\n", (pData->proxyHost == NULL) ? "unset" : (char *)pData->proxyHost);
    dbgprintf("\tproxy port='%d'\n", pData->proxyPort);
    dbgprintf("\tuse https=%d\n", pData->useHttps);
    dbgprintf("\tbatch=%d\n", pData->batchMode);
    dbgprintf("\tbatch.format='%s'\n", pData->batchFormatName);
    dbgprintf("\tbatch.maxbytes=%zu\n", pData->maxBatchBytes);
    dbgprintf("\tbatch.maxsize=%zu\n", pData->maxBatchSize);
    dbgprintf("\treplymaxbytes=%zu\n", pData->replyMaxBytes);
    dbgprintf("\tcompress=%d\n", pData->compress);
    dbgprintf("\tcompress.level=%d\n", pData->compressionLevel);
    dbgprintf("\tallowUnsignedCerts=%d\n", pData->allowUnsignedCerts);
    dbgprintf("\tskipVerifyHost=%d\n", pData->skipVerifyHost);
    dbgprintf("\terrorfile='%s'\n", pData->errorFile == NULL ? (uchar *)"(not configured)" : pData->errorFile);
    dbgprintf("\ttls.cacert='%s'\n", pData->caCertFile);
    dbgprintf("\ttls.mycert='%s'\n", pData->myCertFile);
    dbgprintf("\ttls.myprivkey='%s'\n", pData->myPrivKeyFile);
    dbgprintf("\treloadonhup='%d'\n", pData->reloadOnHup);
    for (i = 0; i < pData->nhttpRetryCodes; ++i) dbgprintf("%c'%d'", i == 0 ? '[' : ' ', pData->httpRetryCodes[i]);
    dbgprintf("]\n");
    dbgprintf("\tretry='%d'\n", pData->retryFailures);
    dbgprintf("\tretry.addmetadata='%d'\n", pData->retryAddMetadata);
    dbgprintf("\tretry.ruleset='%s'\n", pData->retryRulesetName);
    dbgprintf("\tratelimit.interval='%d'\n", pData->ratelimitInterval);
    dbgprintf("\tratelimit.burst='%d'\n", pData->ratelimitBurst);
    for (i = 0; i < pData->nIgnorableCodes; ++i) dbgprintf("%c'%d'", i == 0 ? '[' : ' ', pData->ignorableCodes[i]);
    dbgprintf("]\n");
    dbgprintf("\tratelimit.interval='%d'\n", pData->ratelimitInterval);
    dbgprintf("\tratelimit.burst='%d'\n", pData->ratelimitBurst);
    dbgprintf("\tstatsname='%s'\n", pData->statsName);
    dbgprintf("\tstatsbysenders='%d'\n", pData->statsBySenders);
ENDdbgPrintInstInfo


/* http POST result string ... useful for debugging */
static size_t curlResult(void *ptr, size_t size, size_t nmemb, void *userdata) {
    const char *const p = (const char *)ptr;
    wrkrInstanceData_t *const pWrkrData = (wrkrInstanceData_t *)userdata;
    char *buf;
    size_t size_add;
    size_t newlen;

    PTR_ASSERT_CHK(pWrkrData, WRKR_DATA_TYPE_ES);
    if (nmemb != 0 && size > SIZE_MAX / nmemb) {
        LogError(0, RS_RET_ERR, "omhttp: reply buffer size overflow in curlResult");
        pWrkrData->replyLen = 0;
        if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
            pWrkrData->reply[0] = '\0';
        }
        return 0;
    }
    size_add = size * nmemb;
    if (size_add > SIZE_MAX - pWrkrData->replyLen) {
        LogError(0, RS_RET_ERR, "omhttp: reply buffer size overflow in curlResult");
        pWrkrData->replyLen = 0;
        if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
            pWrkrData->reply[0] = '\0';
        }
        return 0;
    }
    newlen = pWrkrData->replyLen + size_add;
    if (newlen == SIZE_MAX) {
        LogError(0, RS_RET_ERR, "omhttp: reply buffer size overflow in curlResult");
        pWrkrData->replyLen = 0;
        if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
            pWrkrData->reply[0] = '\0';
        }
        return 0;
    }
    if (pWrkrData->pData->replyMaxBytes > 0 && newlen > pWrkrData->pData->replyMaxBytes) {
        LogError(0, RS_RET_ERR, "omhttp: reply buffer exceeds replymaxbytes limit (%zu)",
                 pWrkrData->pData->replyMaxBytes);
        if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
            pWrkrData->reply[pWrkrData->replyLen] = '\0';
        }
        return 0;
    }
    if (newlen + 1 > pWrkrData->replyBufLen) {
        if ((buf = realloc(pWrkrData->reply, newlen + 1)) == NULL) {
            LogError(errno, RS_RET_ERR, "omhttp: realloc failed in curlResult");
            if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
                pWrkrData->reply[pWrkrData->replyLen] = '\0';
            }
            return 0; /* abort due to failure */
        }
        pWrkrData->replyBufLen = newlen + 1;
        pWrkrData->reply = buf;
    }
    memcpy(pWrkrData->reply + pWrkrData->replyLen, p, size_add);
    pWrkrData->replyLen = newlen;
    if (pWrkrData->replyBufLen > 0) {
        pWrkrData->reply[pWrkrData->replyLen] = '\0';
    }
    return size_add;
}

/* Build basic URL part, which includes hostname and port as follows:
 * http://hostname:port/ based on a server param
 * Newly creates a cstr for this purpose.
 * Note: serverParam MUST NOT end in '/' (caller must strip if it exists)
 */
static rsRetVal computeBaseUrl(const char *const serverParam,
                               const int defaultPort,
                               const sbool useHttps,
                               uchar **baseUrl) {
#define SCHEME_HTTPS "https://"
#define SCHEME_HTTP "http://"

    char portBuf[64];
    int r = 0;
    const char *host = serverParam;
    DEFiRet;

    assert(serverParam[strlen(serverParam) - 1] != '/');

    es_str_t *urlBuf = es_newStr(256);
    if (urlBuf == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: failed to allocate es_str urlBuf in computeBaseUrl");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    /* Find where the hostname/ip of the server starts. If the scheme is not specified
     * in the uri, start the buffer with a scheme corresponding to the useHttps parameter.
     */
    if (strcasestr(serverParam, SCHEME_HTTP))
        host = serverParam + strlen(SCHEME_HTTP);
    else if (strcasestr(serverParam, SCHEME_HTTPS))
        host = serverParam + strlen(SCHEME_HTTPS);
    else
        r = useHttps ? es_addBuf(&urlBuf, SCHEME_HTTPS, sizeof(SCHEME_HTTPS) - 1)
                     : es_addBuf(&urlBuf, SCHEME_HTTP, sizeof(SCHEME_HTTP) - 1);

    if (r == 0) r = es_addBuf(&urlBuf, (char *)serverParam, strlen(serverParam));
    if (r == 0 && !strchr(host, ':')) {
        snprintf(portBuf, sizeof(portBuf), ":%d", defaultPort);
        r = es_addBuf(&urlBuf, portBuf, strlen(portBuf));
    }
    if (r == 0) r = es_addChar(&urlBuf, '/');
    if (r == 0) *baseUrl = (uchar *)es_str2cstr(urlBuf, NULL);

    if (r != 0 || *baseUrl == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: error occurred computing baseUrl from server %s", serverParam);
        ABORT_FINALIZE(RS_RET_ERR);
    }
finalize_it:
    if (urlBuf) {
        es_deleteStr(urlBuf);
    }
    RETiRet;
}

static inline void incrementServerIndex(wrkrInstanceData_t *pWrkrData) {
    pWrkrData->serverIndex = (pWrkrData->serverIndex + 1) % pWrkrData->pData->numServers;
}

static inline time_t getFailureRetryTime(const instanceData *const pData, const time_t now) {
    const long delay =
        (pData->healthCheckTimeDelay > 0) ? pData->healthCheckTimeDelay : HTTP_DEFAULT_HEALTHCHECKTIMEDELAY;
    return now + (time_t)delay;
}

static inline time_t getSendFailureRetryTime(const instanceData *const pData, const time_t now) {
    const long delay =
        (pData->healthCheckTimeDelay > 0) ? pData->healthCheckTimeDelay : HTTP_DEFAULT_FAILURE_RETRY_DELAY;
    return now + (time_t)delay;
}

static void ATTR_NONNULL(1) markServerSuspended(instanceData *const pData, const int serverIdx, const time_t now) {
    pthread_mutex_lock(&pData->mutGlobalState);
    pData->serverSuspended[serverIdx] = 1;
    pData->serverNextRetry[serverIdx] = getFailureRetryTime(pData, now);
    pData->serverCheckInProgress[serverIdx] = 0;
    pthread_mutex_unlock(&pData->mutGlobalState);
}

static void ATTR_NONNULL(1) markServerFailed(instanceData *const pData, const int serverIdx, const time_t now) {
    pthread_mutex_lock(&pData->mutGlobalState);
    pData->serverSuspended[serverIdx] = 1;
    pData->serverNextRetry[serverIdx] = getSendFailureRetryTime(pData, now);
    pData->serverCheckInProgress[serverIdx] = 0;
    pthread_mutex_unlock(&pData->mutGlobalState);
}

static void ATTR_NONNULL(1) markServerRateLimited(instanceData *const pData, const int serverIdx, const time_t now) {
    pthread_mutex_lock(&pData->mutGlobalState);
    pData->serverSuspended[serverIdx] = 1;
    pData->serverNextRetry[serverIdx] = getFailureRetryTime(pData, now);
    pthread_mutex_unlock(&pData->mutGlobalState);
}

static void ATTR_NONNULL(1) markServerAvailable(instanceData *const pData, const int serverIdx, const time_t now) {
    pthread_mutex_lock(&pData->mutGlobalState);
    pData->serverSuspended[serverIdx] = 0;
    pData->serverNextRetry[serverIdx] = 0;
    pData->serverLastCheck[serverIdx] = now;
    pData->serverCheckInProgress[serverIdx] = 0;
    pthread_mutex_unlock(&pData->mutGlobalState);
}


/* checks if connection to ES can be established; also iterates over
 * potential servers to support high availability (HA) feature. If it
 * needs to switch server, will record new one in curl handle.
 */
static rsRetVal ATTR_NONNULL() checkConn(wrkrInstanceData_t *const pWrkrData) {
    instanceData *pData = pWrkrData->pData;
    CURLcode res;
    es_str_t *urlBuf = NULL;
    time_t now = time(NULL);
    int i, serverIdx, r;
    int delayedServerIdx = -1;
    int reservedServerIdx = -1;
    DEFiRet;

    if (pData->checkPath == NULL) {
        DBGPRINTF("omhttp: checkConn no health check uri configured skipping it\n");
        FINALIZE;
    }

    pWrkrData->replyLen = 0;

    for (i = 0; i < pData->numServers; ++i) {
        /* Round-robin */
        serverIdx = (pWrkrData->serverIndex + i) % pData->numServers;
        serverData_t *server = pWrkrData->listServerDataWkr[serverIdx];

        pthread_mutex_lock(&pData->mutGlobalState);
        if (pData->serverSuspended[serverIdx]) {
            if (now < pData->serverNextRetry[serverIdx]) {
                pthread_mutex_unlock(&pData->mutGlobalState);
                continue; /* Skip this server globally */
            }
            DBGPRINTF("omhttp: Global suspend expired for server %d, retrying...\n", serverIdx);
        } else if (pData->healthCheckTimeDelay != -1 && pData->serverLastCheck[serverIdx] != 0 &&
                   now < (pData->serverLastCheck[serverIdx] + pData->healthCheckTimeDelay)) {
            pthread_mutex_unlock(&pData->mutGlobalState);
            if (delayedServerIdx == -1) {
                delayedServerIdx = serverIdx;
            }
            continue;
        } else if (pData->serverCheckInProgress[serverIdx]) {
            pthread_mutex_unlock(&pData->mutGlobalState);
            continue;
        }
        pData->serverCheckInProgress[serverIdx] = 1;
        reservedServerIdx = serverIdx;
        pthread_mutex_unlock(&pData->mutGlobalState);

        /* Setup URL and Handle if needed */
        if (server->fullUrlHealth == NULL) {
            pWrkrData->replyLen = 0;
            if (urlBuf != NULL) {
                es_deleteStr(urlBuf);
                urlBuf = NULL;
            }
            urlBuf = es_newStr(256);
            if (urlBuf == NULL) {
                LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: unable to allocate urlBuf buffer.");
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }

            char *serverBase = (char *)pData->serverBaseUrls[serverIdx];
            char *checkPath = (char *)pData->checkPath;

            /* Construct: http://host:port/ + checkPath */
            r = es_addBuf(&urlBuf, serverBase, strlen(serverBase));
            if (r == 0 && checkPath != NULL) {
                /* Ensure we don't double-slash if serverBase ends in / */
                if (strlen(serverBase) > 0 && serverBase[strlen(serverBase) - 1] == '/' && checkPath[0] == '/') {
                    r = es_addBuf(&urlBuf, checkPath + 1, strlen(checkPath) - 1);
                } else {
                    r = es_addBuf(&urlBuf, checkPath, strlen(checkPath));
                }
            }

            if (r == 0) server->fullUrlHealth = (uchar *)es_str2cstr(urlBuf, NULL);

            if (server->fullUrlHealth == NULL) {
                LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: unable to generate healthUrl buffer.");
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }
        }

        curl_easy_setopt(server->curlCheckConnHandle, CURLOPT_URL, (char *)server->fullUrlHealth);

        pWrkrData->replyLen = 0;

        res = curl_easy_perform(server->curlCheckConnHandle);
        if (res == CURLE_OK) {
            markServerAvailable(pData, serverIdx, now);
            reservedServerIdx = -1;
            pWrkrData->serverIndex = serverIdx;
            ABORT_FINALIZE(RS_RET_OK);
        } else {
            markServerSuspended(pData, serverIdx, now);
            reservedServerIdx = -1;
            DBGPRINTF("omhttp: server %d health check FAILED: %s\n", serverIdx, curl_easy_strerror(res));
        }
    }

    if (delayedServerIdx != -1) {
        pWrkrData->serverIndex = delayedServerIdx;
        ABORT_FINALIZE(RS_RET_OK);
    }

    LogMsg(0, RS_RET_SUSPENDED, LOG_WARNING, "omhttp: All servers are globally suspended or unreachable.");
    ABORT_FINALIZE(RS_RET_SUSPENDED);

finalize_it:
    if (reservedServerIdx != -1) {
        pthread_mutex_lock(&pData->mutGlobalState);
        pData->serverCheckInProgress[reservedServerIdx] = 0;
        pthread_mutex_unlock(&pData->mutGlobalState);
    }
    if (urlBuf != NULL) es_deleteStr(urlBuf);
    pWrkrData->replyLen = 0;
    if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
        pWrkrData->reply[0] = '\0';
    }
    RETiRet;
}

BEGINtryResume
    CODESTARTtryResume;
    DBGPRINTF("omhttp: tryResume called\n");
    iRet = checkConn(pWrkrData);
ENDtryResume


/* get the current rest path for this message */
static void ATTR_NONNULL(1) getRestPath(const instanceData *const pData, uchar **const tpls, uchar **const restPath) {
    if (pData->dynRestPath && tpls != NULL) {
        *restPath = tpls[1];
    }

    assert(restPath != NULL);
    return;
}


static rsRetVal ATTR_NONNULL(1) setPostURL(wrkrInstanceData_t *const pWrkrData, uchar **const tpls) {
    DEFiRet;
    instanceData *const pData = pWrkrData->pData;
    serverData_t *serverData = pWrkrData->listServerDataWkr[pWrkrData->serverIndex];
    uchar *restPath = NULL;
    es_str_t *url = NULL;

    /* Determine if we actually need to rebuild the URL.
     * We rebuild if:
     * - dynRestPath is ON (it changes per message)
     * - The cached fullUrlPost is NULL (first time for this server)
     */
    if (pData->dynRestPath || serverData->fullUrlPost == NULL) {
        char *baseUrl = (char *)pData->serverBaseUrls[pWrkrData->serverIndex];
        url = es_newStrFromCStr(baseUrl, strlen(baseUrl));
        if (url == NULL) {
            LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: error allocating estr for POST url.");
            ABORT_FINALIZE(RS_RET_ERR);
        }

        /* Logic: Batch-specific path > Template-generated path > Default config path */
        if (serverData->restPATH != NULL) {
            restPath = serverData->restPATH;
        } else if (pData->dynRestPath) {
            getRestPath(pData, tpls, &restPath);
        } else {
            restPath = pData->restPath;
        }

        if (restPath != NULL) {
            if (es_addBuf(&url, (char *)restPath, ustrlen(restPath)) != 0) {
                LogError(0, RS_RET_ERR, "omhttp: failure appending restPath to URL");
                ABORT_FINALIZE(RS_RET_ERR);
            }
        }

        /* Clean up old cached URL before replacing */
        if (serverData->fullUrlPost != NULL) {
            free(serverData->fullUrlPost);
        }
        serverData->fullUrlPost = (uchar *)es_str2cstr(url, NULL);

        if (serverData->fullUrlPost == NULL) {
            LogError(0, RS_RET_ERR, "omhttp: failure caching URL into serverData");
            ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
        }
    }
    /* 2. Apply the URL to the Curl handle */
    curl_easy_setopt(serverData->curlPostHandle, CURLOPT_URL, (char *)serverData->fullUrlPost);
    DBGPRINTF("omhttp: using REST URL: '%s' (server index %d)\n", serverData->fullUrlPost, pWrkrData->serverIndex);

finalize_it:
    if (url != NULL) {
        es_deleteStr(url);
    }
    RETiRet;
}

/*
 * Dumps entire bulk request and response in error log
 * {
 *	"request": {
 *	 	"url": "https://url.com:443/path",
 *	 	"postdata": "mypayload" }
 *	 "response" : {
 *	 	"status": 400,
 *	 	"response": "error string" }
 * }
 */
static rsRetVal renderJsonErrorMessage(wrkrInstanceData_t *pWrkrData, uchar *reqmsg, char **rendered) {
    DEFiRet;
    fjson_object *req = NULL;
    fjson_object *res = NULL;
    fjson_object *errRoot = NULL;
    uchar *url = pWrkrData->listServerDataWkr[pWrkrData->serverIndex]->fullUrlPost;
    if (url == NULL) {
        url = pWrkrData->pData->serverBaseUrls[pWrkrData->serverIndex];
    }
    if (url == NULL) {
        url = (uchar *)"";
    }

    if ((req = fjson_object_new_object()) == NULL) ABORT_FINALIZE(RS_RET_ERR);
    fjson_object_object_add(req, "url", fjson_object_new_string((char *)url));
    fjson_object_object_add(req, "postdata", fjson_object_new_string((char *)reqmsg));

    if ((res = fjson_object_new_object()) == NULL) {
        fjson_object_put(req);  // cleanup request object
        ABORT_FINALIZE(RS_RET_ERR);
    }

#define ERR_MSG_NULL "NULL: curl request failed or no response"
    fjson_object_object_add(res, "status", fjson_object_new_int(pWrkrData->httpStatusCode));
    if (pWrkrData->reply == NULL) {
        fjson_object_object_add(res, "message", fjson_object_new_string_len(ERR_MSG_NULL, strlen(ERR_MSG_NULL)));
    } else {
        const size_t cappedLen = pWrkrData->replyLen > INT_MAX ? INT_MAX : pWrkrData->replyLen;
        fjson_object_object_add(res, "message", fjson_object_new_string_len(pWrkrData->reply, (int)cappedLen));
    }

    if ((errRoot = fjson_object_new_object()) == NULL) {
        fjson_object_put(req);  // cleanup request object
        fjson_object_put(res);  // cleanup response object
        ABORT_FINALIZE(RS_RET_ERR);
    }

    fjson_object_object_add(errRoot, "request", req);
    fjson_object_object_add(errRoot, "response", res);

    CHKmalloc(*rendered = strdup((char *)fjson_object_to_json_string(errRoot)));

finalize_it:
    if (errRoot != NULL) fjson_object_put(errRoot);

    RETiRet;
}

/* write data error request/replies to separate error file
 * Note: we open the file but never close it before exit. If it
 * needs to be closed, HUP must be sent.
 */
static rsRetVal ATTR_NONNULL()
    writeDataError(wrkrInstanceData_t *const pWrkrData, instanceData *const pData, uchar *const reqmsg) {
    char *rendered = NULL;
    size_t toWrite;
    ssize_t wrRet;
    sbool bMutLocked = 0;

    DEFiRet;

    if (pData->errorFile == NULL) {
        DBGPRINTF(
            "omhttp: no local error logger defined - "
            "ignoring REST error information\n");
        FINALIZE;
    }

    pthread_mutex_lock(&pData->mutErrFile);
    bMutLocked = 1;

    CHKiRet(renderJsonErrorMessage(pWrkrData, reqmsg, &rendered));

    if (pData->fdErrFile == -1) {
        pData->fdErrFile = open((char *)pData->errorFile, O_WRONLY | O_CREAT | O_APPEND | O_LARGEFILE | O_CLOEXEC,
                                S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
        if (pData->fdErrFile == -1) {
            LogError(errno, RS_RET_ERR, "omhttp: error opening error file %s", pData->errorFile);
            ABORT_FINALIZE(RS_RET_ERR);
        }
    }

    /* we do not do real error-handling on the err file, as this finally complicates
     * things way to much.
     */
    DBGPRINTF("omhttp: error record: '%s'\n", rendered);
    toWrite = strlen(rendered) + 1;
    /* Note: we overwrite the '\0' terminator with '\n' -- so we avoid
     * caling malloc() -- write() does NOT need '\0'!
     */
    rendered[toWrite - 1] = '\n'; /* NO LONGER A STRING! */
    wrRet = write(pData->fdErrFile, rendered, toWrite);
    if (wrRet != (ssize_t)toWrite) {
        LogError(errno, RS_RET_IO_ERROR, "omhttp: error writing error file %s, write returned %lld", pData->errorFile,
                 (long long)wrRet);
    }

finalize_it:
    if (bMutLocked) pthread_mutex_unlock(&pData->mutErrFile);
    free(rendered);
    RETiRet;
}

static rsRetVal msgAddResponseMetadata(smsg_t *const __restrict__ pMsg,
                                       wrkrInstanceData_t *const pWrkrData,
                                       size_t batch_index) {
    struct json_object *json = NULL;
    DEFiRet;
    CHKmalloc(json = json_object_new_object());
    /*
        Following metadata is exposed:
        $!omhttp!response!code
        $!omhttp!response!body
        $!omhttp!response!batch_index
    */
    json_object_object_add(json, "code", json_object_new_int(pWrkrData->httpStatusCode));
    if (pWrkrData->reply) {
        const size_t cappedLen = pWrkrData->replyLen > INT_MAX ? INT_MAX : pWrkrData->replyLen;
        json_object_object_add(json, "body", json_object_new_string_len(pWrkrData->reply, (int)cappedLen));
    }
    json_object_object_add(json, "batch_index", json_object_new_int(batch_index));
    struct json_object *const toAdd = json;
    json = NULL;
    CHKiRet(msgAddJSON(pMsg, (uchar *)"!omhttp!response", toAdd, 0, 0));

    /* TODO: possible future, an option to automatically parse to json?
        would be under:
        $!omhttp!response!parsed
    */

finalize_it:
    if (iRet != RS_RET_OK && json) {
        json_object_put(json);
    }
    RETiRet;
}

static rsRetVal queueBatchOnRetryRuleset(wrkrInstanceData_t *const pWrkrData, instanceData *const pData) {
    uchar *msgData;
    smsg_t *pMsg;
    DEFiRet;

    int indexStats = pData->statsBySenders ? pWrkrData->serverIndex : 0;

    if (pData->retryRuleset == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: queueBatchOnRetryRuleset invalid call with a NULL retryRuleset");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    for (size_t i = 0; i < pWrkrData->batch.nmemb; i++) {
        msgData = pWrkrData->batch.data[i];
        DBGPRINTF("omhttp: queueBatchOnRetryRuleset putting message '%s' into retry ruleset '%s'\n", msgData,
                  pData->retryRulesetName);

        // Construct the message object
        CHKiRet(msgConstruct(&pMsg));
        /* These messages are generated by an action worker. In recursive
         * retry-ruleset setups, FULL_DELAY can make that worker block on the
         * same queue it must drain. LIGHT_DELAY still applies short
         * backpressure without waiting indefinitely at the full-delay mark. */
        CHKiRet(MsgSetFlowControlType(pMsg, eFLOWCTL_LIGHT_DELAY));
        MsgSetInputName(pMsg, pInputName);
        MsgSetRawMsg(pMsg, (const char *)msgData, ustrlen(msgData));
        MsgSetMSGoffs(pMsg, 0);  // No header
        MsgSetTAG(pMsg, (const uchar *)"omhttp-retry", 12);

        // And place it on the retry ruleset
        MsgSetRuleset(pMsg, pData->retryRuleset);

        // Add response specific metadata
        if (pData->retryAddMetadata) {
            CHKiRet(msgAddResponseMetadata(pMsg, pWrkrData, i));
        }

        ratelimitAddMsg(pData->ratelimiter, NULL, pMsg);

        // Count here in case not entire batch succeeds
        STATSCOUNTER_INC(pWrkrData->pData->listObjStats[indexStats].ctrMessagesRetry,
                         pWrkrData->pData->listObjStats[indexStats].mutCtrMessagesRetry);
    }
finalize_it:
    RETiRet;
}

static rsRetVal checkResult(wrkrInstanceData_t *pWrkrData, uchar *reqmsg) {
    instanceData *pData;
    long statusCode;
    size_t numMessages;
    sbool rateLimited = 0;
    DEFiRet;
    CURLcode resCurl = 0;
    int indexStats = 0;
    serverData_t *serverData = pWrkrData->listServerDataWkr[pWrkrData->serverIndex];

    pData = pWrkrData->pData;
    statusCode = pWrkrData->httpStatusCode;
    indexStats = pData->statsBySenders ? pWrkrData->serverIndex : 0;
    targetStats_t *const serverStats = &pData->listObjStats[indexStats];

    if (pData->batchMode) {
        numMessages = pWrkrData->batch.nmemb;
    } else {
        numMessages = 1;
    }

    /* HTTP status code handling according to new semantics:
     * - 0xx: Transport/connection errors -> retriable
     * - 1xx/2xx: Success
     * - 3xx: Redirection -> non-retriable (for now, redirect support can be added later)
     * - 4xx: Client errors -> permanent failure (non-retriable)
     * - 5xx: Server errors -> retriable
     */
    if (statusCode == 0) {
        // Transport/connection failure - retriable
        STATSCOUNTER_ADD(serverStats->ctrMessagesFail, serverStats->mutCtrMessagesFail, numMessages);
        STATSCOUNTER_INC(serverStats->ctrHttpRequestsStatus0xx, serverStats->mutCtrHttpRequestsStatus0xx);
        iRet = RS_RET_SUSPENDED;
    } else if (statusCode >= 100 && statusCode < 300) {
        // 1xx (informational) and 2xx (success) - treat as success
        STATSCOUNTER_INC(serverStats->ctrHttpStatusSuccess, serverStats->mutCtrHttpStatusSuccess);
        STATSCOUNTER_ADD(serverStats->ctrMessagesSuccess, serverStats->mutCtrMessagesSuccess, numMessages);

        if (statusCode >= 100 && statusCode < 200) {
            STATSCOUNTER_INC(serverStats->ctrHttpRequestsStatus1xx, serverStats->mutCtrHttpRequestsStatus1xx);
        } else if (statusCode >= 200 && statusCode < 300) {
            STATSCOUNTER_INC(serverStats->ctrHttpRequestsStatus2xx, serverStats->mutCtrHttpRequestsStatus2xx);
        }
        iRet = RS_RET_OK;
    } else if (statusCode >= 300 && statusCode < 400) {
        // 3xx - redirection, treat as permanent failure (non-retriable)
        STATSCOUNTER_INC(serverStats->ctrHttpStatusFail, serverStats->mutCtrHttpStatusFail);
        STATSCOUNTER_ADD(serverStats->ctrMessagesFail, serverStats->mutCtrMessagesFail, numMessages);
        STATSCOUNTER_INC(serverStats->ctrHttpRequestsStatus3xx, serverStats->mutCtrHttpRequestsStatus3xx);
        iRet = RS_RET_DATAFAIL;  // permanent failure
    } else if (statusCode >= 400 && statusCode < 500) {
        STATSCOUNTER_INC(serverStats->ctrHttpStatusFail, serverStats->mutCtrHttpStatusFail);
        STATSCOUNTER_INC(serverStats->ctrHttpRequestsStatus4xx, serverStats->mutCtrHttpRequestsStatus4xx);
        if (statusCode == 429) {
            /* Return SUSPENDED to trigger rsyslog's retry logic */
            STATSCOUNTER_ADD(serverStats->ctrMessagesRetry, serverStats->mutCtrMessagesRetry, numMessages);
            rateLimited = 1;
            iRet = RS_RET_SUSPENDED;
        } else {
            STATSCOUNTER_ADD(serverStats->ctrMessagesFail, serverStats->mutCtrMessagesFail, numMessages);
            iRet = RS_RET_DATAFAIL;  // permanent failure
        }
    } else if (statusCode >= 500) {
        // 5xx - server error, retriable
        STATSCOUNTER_INC(serverStats->ctrHttpStatusFail, serverStats->mutCtrHttpStatusFail);
        STATSCOUNTER_ADD(serverStats->ctrMessagesFail, serverStats->mutCtrMessagesFail, numMessages);
        STATSCOUNTER_INC(serverStats->ctrHttpRequestsStatus5xx, serverStats->mutCtrHttpRequestsStatus5xx);
        iRet = RS_RET_SUSPENDED;
    } else {
        // Unexpected status code
        STATSCOUNTER_INC(serverStats->ctrHttpStatusFail, serverStats->mutCtrHttpStatusFail);
        STATSCOUNTER_ADD(serverStats->ctrMessagesFail, serverStats->mutCtrMessagesFail, numMessages);
        iRet = RS_RET_DATAFAIL;
    }

    // get curl stats for instance
    {
        long req = 0;
        double total = 0;
        /* record total bytes */
        resCurl = curl_easy_getinfo(serverData->curlPostHandle, CURLINFO_REQUEST_SIZE, &req);
        if (!resCurl) {
            STATSCOUNTER_ADD(serverStats->httpRequestsBytes, serverStats->mutHttpRequestsBytes, (uint64_t)req);
        }
        resCurl = curl_easy_getinfo(serverData->curlPostHandle, CURLINFO_TOTAL_TIME, &total);
        if (CURLE_OK == resCurl) {
            /* this needs to be converted to milliseconds */
            long total_time_ms = (long)(total * 1000);
            STATSCOUNTER_ADD(serverStats->httpRequestsTimeMs, serverStats->mutHttpRequestsTimeMs,
                             (uint64_t)total_time_ms);
        }
    }

    /* Check custom retry codes if configured, overriding default behavior */
    if (pData->nhttpRetryCodes > 0) {
        sbool bMatch = 0;
        for (int i = 0; i < pData->nhttpRetryCodes && pData->httpRetryCodes[i] != 0; ++i) {
            if (statusCode == (long)pData->httpRetryCodes[i]) {
                bMatch = 1;
                break;
            }
        }
        if (bMatch) {
            /* Force retry for explicitly configured codes */
            iRet = RS_RET_SUSPENDED;
            if (statusCode == 429) {
                rateLimited = 1;
            }
        }
    }

    // also check if we can mark this as processed
    if (iRet != RS_RET_OK && pData->ignorableCodes) {
        for (int i = 0; i < pData->nIgnorableCodes && pData->ignorableCodes[i] != 0; ++i) {
            if (statusCode == (long)pData->ignorableCodes[i]) {
                iRet = RS_RET_OK;
                rateLimited = 0;
                break;
            }
        }
    }

    if (statusCode == 429 && iRet != RS_RET_OK) {
        iRet = RS_RET_SUSPENDED;
        rateLimited = 1;
    }

    if (iRet != RS_RET_OK) {
        char *serverName = (char *)pData->serverBaseUrls[pWrkrData->serverIndex];
        LogMsg(0, iRet, LOG_ERR, "omhttp: checkResult error http status server: %s code: %ld reply: %s", serverName,
               statusCode, pWrkrData->reply != NULL ? pWrkrData->reply : "NULL");

        writeDataError(pWrkrData, pWrkrData->pData, reqmsg);

        if (iRet == RS_RET_DATAFAIL) {
            /* Permanent failure - don't retry */
            ABORT_FINALIZE(iRet);
        }

        /* Handle retries */
        if (pData->batchMode && pData->maxBatchSize > 1) {
            /* Batch mode: check if retry ruleset is configured */
            if (pData->retryFailures && pData->retryRuleset != NULL) {
                /* Use retry ruleset for batch retry (legacy/advanced mode) */
                rsRetVal retryRet = queueBatchOnRetryRuleset(pWrkrData, pData);
                if (retryRet != RS_RET_OK) {
                    LogMsg(0, retryRet, LOG_ERR,
                           "omhttp: checkResult error while queueing to retry ruleset - "
                           "some messages may be lost");
                }
                /* Don't suspend entire action - we handled individual messages */
                iRet = RS_RET_OK;
            } else {
                /* No retry ruleset - use core retry by returning RS_RET_SUSPENDED */
                /* This is the new default behavior */
                DBGPRINTF("omhttp: batch failed, using core retry mechanism\n");
                /* iRet already set to RS_RET_SUSPENDED */
            }
        } else {
            /* Non-batch mode: use core retry (RS_RET_SUSPENDED already set) */
            DBGPRINTF("omhttp: single message failed, using core retry mechanism\n");
        }
    }

    if (rateLimited) {
        markServerRateLimited(pData, pWrkrData->serverIndex, time(NULL));
    } else if (iRet == RS_RET_SUSPENDED) {
        markServerFailed(pData, pWrkrData->serverIndex, time(NULL));
    } else if (iRet == RS_RET_OK && statusCode >= 100 && statusCode < 300) {
        markServerAvailable(pData, pWrkrData->serverIndex, time(NULL));
    }

finalize_it:
    RETiRet;
}

/* Compress a buffer before sending using zlib. Based on code from tools/omfwd.c
 * Initialize the zstrm object for gzip compression, using this init function.
 * deflateInit2(z_stream strm, int level, int method,
 *                             int windowBits, int memLevel, int strategy);
 * strm: the zlib stream held in pWrkrData
 * level: the compression level held in pData
 * method: the operation constant Z_DEFLATED
 * windowBits: the size of the compression window 15 = log_2(32768)
 *     to configure as gzip add 16 to windowBits (w | 16) for final value 31
 * memLevel: the memory optimization level 8 is default)
 * strategy: using Z_DEFAULT_STRATEGY is default
 */
static rsRetVal compressHttpPayload(wrkrInstanceData_t *pWrkrData, uchar *message, size_t len) {
    int zRet;
    unsigned outavail;
    uchar zipBuf[32 * 1024];

    DEFiRet;

    if (len > UINT_MAX) {
        LogError(0, RS_RET_ERR, "omhttp: payload too large for gzip compression (%zu bytes)", len);
        ABORT_FINALIZE(RS_RET_ERR);
    }

    if (!pWrkrData->bzInitDone) {
        pWrkrData->zstrm.zalloc = Z_NULL;
        pWrkrData->zstrm.zfree = Z_NULL;
        pWrkrData->zstrm.opaque = Z_NULL;
        zRet =
            deflateInit2(&pWrkrData->zstrm, pWrkrData->pData->compressionLevel, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY);
        if (zRet != Z_OK) {
            DBGPRINTF("omhttp: compressHttpPayload error %d returned from zlib/deflateInit2()\n", zRet);
            ABORT_FINALIZE(RS_RET_ZLIB_ERR);
        }
        pWrkrData->bzInitDone = 1;
    } else {
        deflateReset(&pWrkrData->zstrm);
    }

    CHKiRet(resetCompressCtx(pWrkrData, len));

    /* now doing the compression */
    pWrkrData->zstrm.next_in = (Bytef *)message;
    pWrkrData->zstrm.avail_in = (uInt)len;
    /* run deflate() on buffer until everything has been compressed */
    do {
        DBGPRINTF("omhttp: compressHttpPayload in deflate() loop, avail_in %d, total_in %ld\n",
                  pWrkrData->zstrm.avail_in, pWrkrData->zstrm.total_in);
        pWrkrData->zstrm.avail_out = sizeof(zipBuf);
        pWrkrData->zstrm.next_out = zipBuf;

        zRet = deflate(&pWrkrData->zstrm, Z_NO_FLUSH);
        DBGPRINTF("omhttp: compressHttpPayload after deflate, ret %d, avail_out %d\n", zRet,
                  pWrkrData->zstrm.avail_out);
        if (zRet != Z_OK) ABORT_FINALIZE(RS_RET_ZLIB_ERR);
        outavail = sizeof(zipBuf) - pWrkrData->zstrm.avail_out;
        if (outavail != 0) CHKiRet(appendCompressCtx(pWrkrData, zipBuf, outavail));

    } while (pWrkrData->zstrm.avail_out == 0);

    /* run deflate again with Z_FINISH with no new input */
    pWrkrData->zstrm.avail_in = 0;
    do {
        pWrkrData->zstrm.avail_out = sizeof(zipBuf);
        pWrkrData->zstrm.next_out = zipBuf;
        deflate(&pWrkrData->zstrm, Z_FINISH); /* returns Z_STREAM_END == 1 */
        outavail = sizeof(zipBuf) - pWrkrData->zstrm.avail_out;
        if (outavail != 0) CHKiRet(appendCompressCtx(pWrkrData, zipBuf, outavail));

    } while (pWrkrData->zstrm.avail_out == 0);

finalize_it:
    if (pWrkrData->bzInitDone) deflateEnd(&pWrkrData->zstrm);
    pWrkrData->bzInitDone = 0;
    RETiRet;
}

static void ATTR_NONNULL() initCompressCtx(wrkrInstanceData_t *pWrkrData) {
    pWrkrData->compressCtx.buf = NULL;
    pWrkrData->compressCtx.curLen = 0;
    pWrkrData->compressCtx.len = 0;
}

static void ATTR_NONNULL() freeCompressCtx(wrkrInstanceData_t *pWrkrData) {
    if (pWrkrData->compressCtx.buf != NULL) {
        free(pWrkrData->compressCtx.buf);
        pWrkrData->compressCtx.buf = NULL;
    }
}


static rsRetVal ATTR_NONNULL() resetCompressCtx(wrkrInstanceData_t *pWrkrData, size_t len) {
    DEFiRet;
    pWrkrData->compressCtx.curLen = 0;
    pWrkrData->compressCtx.len = len;
    CHKiRet(growCompressCtx(pWrkrData, len));

finalize_it:
    if (iRet != RS_RET_OK) freeCompressCtx(pWrkrData);
    RETiRet;
}

static rsRetVal ATTR_NONNULL() growCompressCtx(wrkrInstanceData_t *pWrkrData, size_t newLen) {
    DEFiRet;
    if (pWrkrData->compressCtx.buf == NULL) {
        CHKmalloc(pWrkrData->compressCtx.buf = (uchar *)malloc(sizeof(uchar) * newLen));
    } else {
        uchar *const newbuf = (uchar *)realloc(pWrkrData->compressCtx.buf, sizeof(uchar) * newLen);
        CHKmalloc(newbuf);
        pWrkrData->compressCtx.buf = newbuf;
    }
    pWrkrData->compressCtx.len = newLen;
finalize_it:
    RETiRet;
}

static rsRetVal ATTR_NONNULL() appendCompressCtx(wrkrInstanceData_t *pWrkrData, uchar *srcBuf, size_t srcLen) {
    size_t newLen;
    DEFiRet;
    newLen = pWrkrData->compressCtx.curLen + srcLen;
    if (newLen > pWrkrData->compressCtx.len) CHKiRet(growCompressCtx(pWrkrData, newLen));

    memcpy(pWrkrData->compressCtx.buf + pWrkrData->compressCtx.curLen, srcBuf, srcLen);
    pWrkrData->compressCtx.curLen = newLen;
finalize_it:
    if (iRet != RS_RET_OK) freeCompressCtx(pWrkrData);
    RETiRet;
}

/* Some duplicate code to curlSetup, but we need to add the gzip content-encoding
 * header at runtime, and if the compression fails, we do not want to send it.
 * Additionally, the curlCheckConnHandle should not be configured with a gzip header.
 */
/* Local macro to safely append headers and handle OOM without
 * losing the already-built list.
 */
#define SAFE_APPEND(header_str)                      \
    do {                                             \
        temp = curl_slist_append(slist, header_str); \
        if (temp == NULL) {                          \
            ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);    \
        }                                            \
        slist = temp;                                \
    } while (0);

static rsRetVal ATTR_NONNULL() buildCurlHeaders(wrkrInstanceData_t *pWrkrData,
                                                serverData_t *serverData,
                                                HEADER **targetHeader,
                                                sbool contentEncodeGzip) {
    struct curl_slist *slist = NULL;
    /* Optimization: Temp pointer to capture return without losing list on error */
    struct curl_slist *temp = NULL;
    DEFiRet;

    /* 1. Set Content-Type Header */
    if (pWrkrData->pData->httpcontenttype != NULL) {
        // If content type specified use it, otherwise use a sane default
        SAFE_APPEND((char *)pWrkrData->pData->headerContentTypeBuf);
    } else {
        if (pWrkrData->pData->batchMode) {
            // If in batch mode, use the appropriate content type header for the format
            switch (pWrkrData->pData->batchFormat) {
                case FMT_JSONARRAY:
                    SAFE_APPEND(HTTP_HEADER_CONTENT_JSON);
                    break;
                case FMT_KAFKAREST:
                    SAFE_APPEND(HTTP_HEADER_CONTENT_KAFKA);
                    break;
                case FMT_NEWLINE:
                    SAFE_APPEND(HTTP_HEADER_CONTENT_TEXT);
                    break;
                case FMT_LOKIREST:
                    SAFE_APPEND(HTTP_HEADER_CONTENT_JSON);
                    break;
                default:
                    SAFE_APPEND(HTTP_HEADER_CONTENT_TEXT);
            }
        } else {
            // Otherwise non batch, presume most users are sending JSON
            SAFE_APPEND(HTTP_HEADER_CONTENT_JSON);
        }
    }

    /* 2. Add Configured Header (Single Key-Value) */
    if (pWrkrData->pData->headerBuf != NULL) {
        SAFE_APPEND((char *)pWrkrData->pData->headerBuf);
    }

    /* 3. Add Configured Headers (Array) */
    for (int k = 0; k < pWrkrData->pData->nHttpHeaders; k++) {
        SAFE_APPEND((char *)pWrkrData->pData->httpHeaders[k]);
    }

    /* 4. Add Expect Header */
    // When sending more than 1Kb, libcurl automatically sends an Except: 100-Continue header
    // and will wait 1s for a response. We disable this behavior.
    SAFE_APPEND(HTTP_HEADER_EXPECT_EMPTY);

    /* 5. Add Compression Header */
    if (contentEncodeGzip) {
        SAFE_APPEND(HTTP_HEADER_ENCODING_GZIP);
    }

    curl_slist_free_all(*targetHeader);
    *targetHeader = slist;
    slist = NULL;
    serverData->gzipHeaderEnabled = contentEncodeGzip;

finalize_it:
    if (iRet != RS_RET_OK) {
        /* If we failed halfway, free the list we were building. */
        if (slist != NULL) curl_slist_free_all(slist);
        LogError(0, iRet, "omhttp: error allocating curl header slist");
    }
    RETiRet;
}
#undef SAFE_APPEND


static rsRetVal ATTR_NONNULL(1, 2) curlPost(wrkrInstanceData_t *pWrkrData,
                                            uchar *message,
                                            size_t msglen,
                                            uchar **tpls,
                                            const int nmsgs __attribute__((unused))) {
    CURLcode curlCode;
    CURL *curl = NULL;
    char errbuf[CURL_ERROR_SIZE] = "";
    int indexStats;
    serverData_t *serverData;
    char *postData;
    size_t postLen;
    curl_off_t postLenCurl;
    sbool compressed;
    DEFiRet;

    PTR_ASSERT_SET_TYPE(pWrkrData, WRKR_DATA_TYPE_ES);

    if (pWrkrData->pData->numServers > 1) {
        DBGPRINTF("omhttp: checkConn has to be done\n");
        /* needs to be called to support ES HA feature */
        CHKiRet(checkConn(pWrkrData));
    }

    indexStats = pWrkrData->pData->statsBySenders ? pWrkrData->serverIndex : 0;
    serverData = pWrkrData->listServerDataWkr[pWrkrData->serverIndex];

    pWrkrData->replyLen = 0;
    pWrkrData->httpStatusCode = 0;
    if (pWrkrData->reply != NULL && pWrkrData->replyBufLen > 0) {
        pWrkrData->reply[0] = '\0';
    }

    postData = (char *)message;
    postLen = msglen;
    compressed = 0;

    if (pWrkrData->pData->compress) {
        iRet = compressHttpPayload(pWrkrData, message, msglen);
        if (iRet != RS_RET_OK) {
            LogError(0, iRet, "omhttp: curlPost error while compressing, will default to uncompressed");
        } else {
            postData = (char *)pWrkrData->compressCtx.buf;
            postLen = pWrkrData->compressCtx.curLen;
            compressed = 1;
            DBGPRINTF("omhttp: curlPost compressed %zu to %zu bytes\n", msglen, postLen);
        }
    }

    /*
     * Set URL into the Curl Handle if :
     *    - Dynrestpath is set (need to be set each time)
     *    - If first request and fullUrlPost not created
     */
    if (pWrkrData->pData->dynRestPath || serverData->fullUrlPost == NULL) {
        CHKiRet(setPostURL(pWrkrData, tpls));

        /*
         * Set Header if first request to the destination server
         */
        if (serverData->curlHeader == NULL) {
            CHKiRet(buildCurlHeaders(pWrkrData, serverData, &serverData->curlHeader, compressed));
            curl_easy_setopt(serverData->curlPostHandle, CURLOPT_HTTPHEADER, serverData->curlHeader);
        }
    }

    if (serverData->curlHeader == NULL || serverData->gzipHeaderEnabled != compressed) {
        CHKiRet(buildCurlHeaders(pWrkrData, serverData, &serverData->curlHeader, compressed));
        curl_easy_setopt(serverData->curlPostHandle, CURLOPT_HTTPHEADER, serverData->curlHeader);
    }

    /* Set Curl object here */
    curl = serverData->curlPostHandle;
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData);
    if (postLen > (size_t)LLONG_MAX) {
        LogError(0, RS_RET_ERR, "omhttp: POST payload too large for libcurl (%zu bytes)", postLen);
        ABORT_FINALIZE(RS_RET_ERR);
    }
    postLenCurl = (curl_off_t)postLen;
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postLenCurl);
    curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);

    curlCode = curl_easy_perform(curl);
    DBGPRINTF("omhttp: curlPost curl returned %lld\n", (long long)curlCode);
    STATSCOUNTER_INC(pWrkrData->pData->listObjStats[indexStats].ctrHttpRequestsCount,
                     pWrkrData->pData->listObjStats[indexStats].mutCtrHttpRequestsCount);

    if (curlCode != CURLE_OK) {
        STATSCOUNTER_INC(pWrkrData->pData->listObjStats[indexStats].ctrHttpRequestFail,
                         pWrkrData->pData->listObjStats[indexStats].mutCtrHttpRequestFail);
        LogError(0, RS_RET_SUSPENDED, "omhttp: suspending ourselves due to server failure %lld: %s",
                 (long long)curlCode, errbuf);
        markServerFailed(pWrkrData->pData, pWrkrData->serverIndex, time(NULL));
        // Check the result here too and retry if needed, then we should suspend
        // Usually in batch mode we clobber any iRet values, but probably not a great
        // idea to keep hitting a dead server. The http status code will be 0 at this point.
        checkResult(pWrkrData, message);
        ABORT_FINALIZE(RS_RET_SUSPENDED);
    } else {
        STATSCOUNTER_INC(pWrkrData->pData->listObjStats[indexStats].ctrHttpRequestSuccess,
                         pWrkrData->pData->listObjStats[indexStats].mutCtrHttpRequestSuccess);
    }

    // Grab the HTTP Response code
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &pWrkrData->httpStatusCode);
    if (pWrkrData->reply == NULL) {
        DBGPRINTF("omhttp: curlPost pWrkrData reply==NULL, replyLen = '%llu'\n",
                  (unsigned long long)pWrkrData->replyLen);
    } else {
        DBGPRINTF("omhttp: curlPost pWrkrData replyLen = '%llu'\n", (unsigned long long)pWrkrData->replyLen);
        if (pWrkrData->replyBufLen > 0) {
            pWrkrData->reply[pWrkrData->replyLen] = '\0';
            /* Append 0 byte when buffer is allocated to keep reply safely NUL-terminated */
        }
        DBGPRINTF("omhttp: curlPost pWrkrData reply: '%s'\n", pWrkrData->reply);
    }
    CHKiRet(checkResult(pWrkrData, message));

finalize_it:
    incrementServerIndex(pWrkrData);
    pWrkrData->replyLen = 0;
    RETiRet;
}

/* Build a JSON batch that conforms to the Kafka Rest Proxy format.
 * See https://docs.confluent.io/current/kafka-rest/docs/quickstart.html for more info.
 * Want {"records": [{"value": "message1"}, {"value": "message2"}]}
 */
static rsRetVal serializeBatchKafkaRest(wrkrInstanceData_t *pWrkrData, char **batchBuf) {
    fjson_object *batchArray = NULL;
    fjson_object *recordObj = NULL;
    fjson_object *valueObj = NULL;
    fjson_object *msgObj = NULL;

    size_t numMessages = pWrkrData->batch.nmemb;
    size_t sizeTotal = pWrkrData->batch.sizeBytes + numMessages + 1;  // messages + brackets + commas
    DBGPRINTF("omhttp: serializeBatchKafkaRest numMessages=%zd sizeTotal=%zd\n", numMessages, sizeTotal);

    DEFiRet;

    batchArray = fjson_object_new_array();
    if (batchArray == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: serializeBatchKafkaRest failed to create array");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    for (size_t i = 0; i < numMessages; i++) {
        valueObj = fjson_object_new_object();
        if (valueObj == NULL) {
            fjson_object_put(batchArray);  // cleanup
            LogError(0, RS_RET_ERR, "omhttp: serializeBatchKafkaRest failed to create value object");
            ABORT_FINALIZE(RS_RET_ERR);
        }

        msgObj = fjson_tokener_parse((char *)pWrkrData->batch.data[i]);
        if (msgObj == NULL) {
            LogError(0, NO_ERRCODE, "omhttp: serializeBatchKafkaRest failed to parse %s as json ignoring it",
                     pWrkrData->batch.data[i]);
            continue;
        }
        fjson_object_object_add(valueObj, "value", msgObj);
        fjson_object_array_add(batchArray, valueObj);
    }

    recordObj = fjson_object_new_object();
    if (recordObj == NULL) {
        fjson_object_put(batchArray);  // cleanup
        LogError(0, RS_RET_ERR, "omhttp: serializeBatchKafkaRest failed to create record object");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    fjson_object_object_add(recordObj, "records", batchArray);

    const char *batchString = fjson_object_to_json_string_ext(recordObj, FJSON_TO_STRING_PLAIN);
    *batchBuf = strndup(batchString, strlen(batchString));

finalize_it:
    if (recordObj != NULL) {
        fjson_object_put(recordObj);
        recordObj = NULL;
    }

    RETiRet;
}

static rsRetVal serializeBatchLokiRest(wrkrInstanceData_t *pWrkrData, char **batchBuf) {
    fjson_object *batchArray = NULL;
    fjson_object *recordObj = NULL;
    fjson_object *msgObj = NULL;

    size_t numMessages = pWrkrData->batch.nmemb;
    size_t sizeTotal = pWrkrData->batch.sizeBytes + numMessages + 1;  // messages + brackets + commas
    DBGPRINTF("omhttp: serializeBatchLokiRest numMessages=%zd sizeTotal=%zd\n", numMessages, sizeTotal);

    DEFiRet;

    batchArray = fjson_object_new_array();
    if (batchArray == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: serializeBatchLokiRest failed to create array");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    for (size_t i = 0; i < numMessages; i++) {
        DBGPRINTF("omhttp: serializeBatchLokiRest parsing message [%s]\n", (char *)pWrkrData->batch.data[i]);
        msgObj = fjson_tokener_parse((char *)pWrkrData->batch.data[i]);
        if (msgObj == NULL) {
            LogError(0, NO_ERRCODE, "omhttp: serializeBatchLokiRest failed to parse %s as json ignoring it",
                     pWrkrData->batch.data[i]);
            continue;
        }
        fjson_object_array_add(batchArray, msgObj);
    }

    recordObj = fjson_object_new_object();
    if (recordObj == NULL) {
        fjson_object_put(batchArray);  // cleanup
        LogError(0, RS_RET_ERR, "omhttp: serializeBatchLokiRest failed to create record object");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    fjson_object_object_add(recordObj, "streams", batchArray);

    const char *batchString = fjson_object_to_json_string_ext(recordObj, FJSON_TO_STRING_PLAIN);
    *batchBuf = strndup(batchString, strlen(batchString));

finalize_it:
    if (recordObj != NULL) {
        fjson_object_put(recordObj);
        recordObj = NULL;
    }

    RETiRet;
}

/* Build a JSON batch by placing each element in an array.
 */
static rsRetVal serializeBatchJsonArray(wrkrInstanceData_t *pWrkrData, char **batchBuf) {
    fjson_object *batchArray = NULL;
    fjson_object *msgObj = NULL;
    size_t numMessages = pWrkrData->batch.nmemb;
    size_t sizeTotal = pWrkrData->batch.sizeBytes + numMessages + 1;  // messages + brackets + commas
    DBGPRINTF("omhttp: serializeBatchJsonArray numMessages=%zd sizeTotal=%zd\n", numMessages, sizeTotal);

    DEFiRet;

    batchArray = fjson_object_new_array();
    if (batchArray == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: serializeBatchJsonArray failed to create array");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    for (size_t i = 0; i < numMessages; i++) {
        msgObj = fjson_tokener_parse((char *)pWrkrData->batch.data[i]);
        if (msgObj == NULL) {
            LogError(0, NO_ERRCODE, "omhttp: serializeBatchJsonArray failed to parse %s as json, ignoring it",
                     pWrkrData->batch.data[i]);
            continue;
        }
        fjson_object_array_add(batchArray, msgObj);
    }

    const char *batchString = fjson_object_to_json_string_ext(batchArray, FJSON_TO_STRING_PLAIN);
    *batchBuf = strndup(batchString, strlen(batchString));

finalize_it:
    if (batchArray != NULL) {
        fjson_object_put(batchArray);
        batchArray = NULL;
    }
    RETiRet;
}

/* Build a batch by joining each element with a newline character.
 */
static rsRetVal serializeBatchNewline(wrkrInstanceData_t *pWrkrData, char **batchBuf) {
    DEFiRet;
    size_t numMessages = pWrkrData->batch.nmemb;
    size_t sizeTotal = pWrkrData->batch.sizeBytes + numMessages;  // message + newline + null term
    int r = 0;

    DBGPRINTF("omhttp: serializeBatchNewline numMessages=%zd sizeTotal=%zd\n", numMessages, sizeTotal);

    es_str_t *batchString = es_newStr(1024);

    if (batchString == NULL) ABORT_FINALIZE(RS_RET_ERR);

    for (size_t i = 0; i < numMessages; i++) {
        size_t nToCopy = ustrlen(pWrkrData->batch.data[i]);
        if (r == 0) r = es_addBuf(&batchString, (char *)pWrkrData->batch.data[i], nToCopy);
        if (i == numMessages - 1) break;
        if (r == 0) r = es_addChar(&batchString, '\n');
    }

    if (r == 0) *batchBuf = (char *)es_str2cstr(batchString, NULL);

    if (r != 0 || *batchBuf == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: serializeBatchNewline failed to build batch string");
        ABORT_FINALIZE(RS_RET_ERR);
    }

finalize_it:
    if (batchString != NULL) es_deleteStr(batchString);

    RETiRet;
}

/* Return the final batch size in bytes for each serialization method.
 * Used to decide if a batch should be flushed early.
 */
static size_t computeBatchSize(wrkrInstanceData_t *pWrkrData) {
    size_t extraBytes = 0;
    size_t sizeBytes = pWrkrData->batch.sizeBytes;
    size_t numMessages = pWrkrData->batch.nmemb;

    switch (pWrkrData->pData->batchFormat) {
        case FMT_JSONARRAY:
            // square brackets, commas between each message
            // 2 + numMessages - 1 = numMessages + 1
            extraBytes = numMessages > 0 ? numMessages + 1 : 2;
            break;
        case FMT_KAFKAREST:
            // '{}', '[]', '"records":'= 2 + 2 + 10 = 14
            // '{"value":}' for each message = n * 10
            // plus commas between array elements (n > 0 ? n - 1 : 0)
            extraBytes = (numMessages * 10) + 14 + (numMessages > 0 ? numMessages - 1 : 0);
            break;
        case FMT_NEWLINE:
            // newlines between each message
            extraBytes = numMessages > 0 ? numMessages - 1 : 0;
            break;
        case FMT_LOKIREST:
            // {"streams":[ '{}', '[]', '"streams":' = 14
            //    {"stream": {key:value}..., "values":[[timestamp: msg1]]},
            //    {"stream": {key:value}..., "values":[[timestamp: msg2]]}
            // ]}
            // Approximate per-message wrapper overhead (2) plus commas between elements
            extraBytes = (numMessages * 2) + 14 + (numMessages > 0 ? numMessages - 1 : 0);
            break;
        default:
            // newlines between each message
            extraBytes = numMessages > 0 ? numMessages - 1 : 0;
    }

    return sizeBytes + extraBytes + 1;  // plus a null
}

/* Return the delta of extra bytes added by appending one more message to
 * the current batch, based on the configured serialization format.
 */
static inline size_t computeDeltaExtraOnAppend(const wrkrInstanceData_t *pWrkrData) {
    const size_t numMessages = pWrkrData->batch.nmemb;
    switch (pWrkrData->pData->batchFormat) {
        case FMT_JSONARRAY:
            /* add a comma if there is already at least one element */
            return (numMessages > 0) ? 1 : 0;
        case FMT_KAFKAREST:
            /* per-message wrapper overhead (e.g. {"value":}) + comma if needed */
            return 10 + ((numMessages > 0) ? 1 : 0);
        case FMT_NEWLINE:
            /* add a newline if there is already at least one element */
            return (numMessages > 0) ? 1 : 0;
        case FMT_LOKIREST:
            /* approximate per-message overhead in wrapper + comma if needed */
            return 2 + ((numMessages > 0) ? 1 : 0);
        default:
            return (numMessages > 0) ? 1 : 0;
    }
}

static void ATTR_NONNULL() initializeBatch(wrkrInstanceData_t *pWrkrData) {
    pWrkrData->batch.sizeBytes = 0;
    pWrkrData->batch.nmemb = 0;
}

/* Adds a message to this worker's batch
 */
static rsRetVal buildBatch(wrkrInstanceData_t *pWrkrData, uchar *message) {
    DEFiRet;

    if (pWrkrData->batch.nmemb >= pWrkrData->pData->maxBatchSize) {
        LogError(0, RS_RET_ERR,
                 "omhttp: buildBatch something has gone wrong,"
                 "number of messages in batch is bigger than the max batch size, bailing");
        ABORT_FINALIZE(RS_RET_ERR);
    }
    pWrkrData->batch.data[pWrkrData->batch.nmemb] = message;
    pWrkrData->batch.sizeBytes += strlen((char *)message);
    pWrkrData->batch.nmemb++;

finalize_it:
    RETiRet;
}

static rsRetVal submitBatch(wrkrInstanceData_t *pWrkrData, uchar **tpls) {
    DEFiRet;
    char *batchBuf = NULL;

    switch (pWrkrData->pData->batchFormat) {
        case FMT_JSONARRAY:
            iRet = serializeBatchJsonArray(pWrkrData, &batchBuf);
            break;
        case FMT_KAFKAREST:
            iRet = serializeBatchKafkaRest(pWrkrData, &batchBuf);
            break;
        case FMT_LOKIREST:
            iRet = serializeBatchLokiRest(pWrkrData, &batchBuf);
            break;
        case FMT_NEWLINE:
            iRet = serializeBatchNewline(pWrkrData, &batchBuf);
            break;
        default:
            iRet = serializeBatchNewline(pWrkrData, &batchBuf);
    }

    if (iRet != RS_RET_OK || batchBuf == NULL) ABORT_FINALIZE(iRet);

    DBGPRINTF("omhttp: submitBatch, batch: '%s' tpls: '%p'\n", batchBuf, tpls);

    CHKiRet(curlPost(pWrkrData, (uchar *)batchBuf, strlen(batchBuf), tpls, pWrkrData->batch.nmemb));

finalize_it:
    if (batchBuf != NULL) free(batchBuf);
    RETiRet;
}

static rsRetVal submitBatchDatafailOk(wrkrInstanceData_t *pWrkrData, uchar **tpls) {
    DEFiRet;
    const size_t nmemb = pWrkrData->batch.nmemb;
    const rsRetVal submitRet = submitBatch(pWrkrData, tpls);
    if (submitRet == RS_RET_DATAFAIL) {
        DBGPRINTF("omhttp: permanent data failure for batch of %zu message(s), continuing transaction\n", nmemb);
        FINALIZE;
    }
    CHKiRet(submitRet);

finalize_it:
    RETiRet;
}

BEGINbeginTransaction
    CODESTARTbeginTransaction;
    if (!pWrkrData->pData->batchMode) {
        FINALIZE;
    }

    initializeBatch(pWrkrData);
finalize_it:
ENDbeginTransaction

BEGINcommitTransaction
    unsigned i;
    size_t nBytes;
    sbool submit;
    CODESTARTcommitTransaction;
    instanceData *const pData = pWrkrData->pData;
    const int iNumTpls = pData->dynRestPath ? 2 : 1;
    int indexStats = pWrkrData->pData->statsBySenders ? pWrkrData->serverIndex : 0;


    for (i = 0; i < nParams; ++i) {
        uchar *payload = actParam(pParams, iNumTpls, i, 0).param;
        uchar *tpls[2] = {payload, NULL};
        serverData_t *serverData = pWrkrData->listServerDataWkr[pWrkrData->serverIndex];

        if (iNumTpls == 2) tpls[1] = actParam(pParams, iNumTpls, i, 1).param;

        STATSCOUNTER_INC(pWrkrData->pData->listObjStats[indexStats].ctrMessagesSubmitted,
                         pWrkrData->pData->listObjStats[indexStats].mutCtrMessagesSubmitted);

        if (pData->batchMode) {
            if (pData->dynRestPath) {
                uchar *restPath = actParam(pParams, iNumTpls, i, 1).param;
                if (serverData->restPATH == NULL) {
                    CHKmalloc(serverData->restPATH = (uchar *)strdup((char *)restPath));
                } else if (strcmp((char *)serverData->restPATH, (char *)restPath) != 0) {
                    /* restPath changed -> flush current batch if it contains data */
                    if (pWrkrData->batch.nmemb > 0) {
                        CHKiRet(submitBatchDatafailOk(pWrkrData, NULL));
                        serverData = pWrkrData->listServerDataWkr[pWrkrData->serverIndex];
                    }
                    initializeBatch(pWrkrData);
                    free(serverData->restPATH);
                    CHKmalloc(serverData->restPATH = (uchar *)strdup((char *)restPath));
                }
            }

            /* If maxBatchSize is 1, immediately build and post a single-element batch */
            if (pData->maxBatchSize == 1) {
                initializeBatch(pWrkrData);
                CHKiRet(buildBatch(pWrkrData, payload));
                CHKiRet(submitBatchDatafailOk(pWrkrData, tpls));
                initializeBatch(pWrkrData);
                continue;
            }

            /* Determine if we should submit due to size/bytes thresholds */
            nBytes = ustrlen((char *)payload);
            submit = 0;
            if (pWrkrData->batch.nmemb >= pData->maxBatchSize) {
                submit = 1;
                DBGPRINTF("omhttp: maxbatchsize limit reached submitting batch of %zd elements.\n",
                          pWrkrData->batch.nmemb);
            } else {
                const size_t predicted = computeBatchSize(pWrkrData) + nBytes + computeDeltaExtraOnAppend(pWrkrData);
                if (predicted > pData->maxBatchBytes) {
                    submit = 1;
                    DBGPRINTF("omhttp: maxbytes limit reached submitting partial batch of %zd elements.\n",
                              pWrkrData->batch.nmemb);
                }
            }

            if (submit) {
                /* flush current batch, then start a new one. keep dyn restPath consistent */
                CHKiRet(submitBatchDatafailOk(pWrkrData, NULL));
                initializeBatch(pWrkrData);
                if (pData->dynRestPath) {
                    uchar *restPath = actParam(pParams, iNumTpls, i, 1).param;
                    serverData = pWrkrData->listServerDataWkr[pWrkrData->serverIndex];
                    if (serverData->restPATH == NULL || strcmp((char *)serverData->restPATH, (char *)restPath) != 0) {
                        free(serverData->restPATH);
                        CHKmalloc(serverData->restPATH = (uchar *)strdup((char *)restPath));
                    }
                }
            }
            CHKiRet(buildBatch(pWrkrData, payload));
        } else {
            /* non-batch mode: send immediately */
            const rsRetVal postRet = curlPost(pWrkrData, payload, strlen((char *)payload), tpls, 1);
            if (postRet == RS_RET_DATAFAIL) {
                DBGPRINTF("omhttp: permanent data failure for single message, continuing transaction\n");
                continue;
            }
            CHKiRet(postRet);
        }
    }

    /* finalize any remaining batch data */
    if (pData->batchMode) {
        if (pWrkrData->batch.nmemb > 0) {
            CHKiRet(submitBatchDatafailOk(pWrkrData, NULL));
        } else {
            dbgprintf("omhttp: commitTransaction, pWrkrData->batch.nmemb = 0, nothing to send. \n");
        }
    }
finalize_it:
ENDcommitTransaction


/* Creates authentication header uid:pwd
 */
static rsRetVal computeAuthHeader(char *uid, char *pwd, uchar **authBuf) {
    int r;
    DEFiRet;

    es_str_t *auth = es_newStr(1024);
    if (auth == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: failed to allocate es_str auth for auth header construction");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    r = es_addBuf(&auth, uid, strlen(uid));
    if (r == 0) r = es_addChar(&auth, ':');
    if (r == 0 && pwd != NULL) r = es_addBuf(&auth, pwd, strlen(pwd));
    if (r == 0) *authBuf = (uchar *)es_str2cstr(auth, NULL);

    if (r != 0 || *authBuf == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: failed to build auth header\n");
        ABORT_FINALIZE(RS_RET_ERR);
    }

finalize_it:
    if (auth != NULL) es_deleteStr(auth);
    RETiRet;
}

static rsRetVal computeApiHeader(char *key, char *value, uchar **headerBuf) {
    int r;
    DEFiRet;

    es_str_t *header = es_newStr(10240);
    if (header == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: failed to allocate es_str auth for api header construction");
        ABORT_FINALIZE(RS_RET_ERR);
    }

    r = es_addBuf(&header, key, strlen(key));
    if (r == 0) r = es_addChar(&header, ':');
    if (r == 0) r = es_addChar(&header, ' ');
    if (r == 0 && value != NULL) r = es_addBuf(&header, value, strlen(value));
    if (r == 0) *headerBuf = (uchar *)es_str2cstr(header, NULL);

    if (r != 0 || *headerBuf == NULL) {
        LogError(0, RS_RET_ERR, "omhttp: failed to build http header\n");
        ABORT_FINALIZE(RS_RET_ERR);
    }

finalize_it:
    if (header != NULL) es_deleteStr(header);
    RETiRet;
}

static void ATTR_NONNULL()
    curlSetupCommon(wrkrInstanceData_t *const pWrkrData, serverData_t *const serverData, CURL *const handle) {
    instanceData *const pData = pWrkrData->pData;
    CURLcode cRet;
    PTR_ASSERT_SET_TYPE(pWrkrData, WRKR_DATA_TYPE_ES);
    curl_easy_setopt(handle, CURLOPT_HTTPHEADER, serverData->curlHeader);
    curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
    curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, curlResult);
    curl_easy_setopt(handle, CURLOPT_WRITEDATA, pWrkrData);
    if (pData->proxyHost != NULL) {
        curl_easy_setopt(handle, CURLOPT_PROXY, pData->proxyHost);
    }
    if (pData->proxyPort != 0) {
        curl_easy_setopt(handle, CURLOPT_PROXYPORT, (long)pData->proxyPort);
    }
    if (pData->restPathTimeout) {
        curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, pData->restPathTimeout);
    }
    if (pData->allowUnsignedCerts) curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
    if (pData->skipVerifyHost) curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
    if (pData->authBuf != NULL) {
        curl_easy_setopt(handle, CURLOPT_USERPWD, pData->authBuf);
        curl_easy_setopt(handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
    }
    if (pData->caCertFile) curl_easy_setopt(handle, CURLOPT_CAINFO, pData->caCertFile);
    if (pData->myCertFile) curl_easy_setopt(handle, CURLOPT_SSLCERT, pData->myCertFile);
    if (pData->myPrivKeyFile) curl_easy_setopt(handle, CURLOPT_SSLKEY, pData->myPrivKeyFile);

    /* Set up a user-agent for HTTP keep alive working with Splunk HEC */
    if (pData->vendor == SPLUNK) {
        cRet = curl_easy_setopt(handle, CURLOPT_USERAGENT, SPLUNK_HEC_USER_AGENT);
        if (cRet != CURLE_OK) DBGPRINTF("omhttp: curlSetupCommon unknown option CURLOPT_USERAGENT\n");
    }
    /* Force Cache session for SSL */
    if (pData->useHttps) {
        cRet = curl_easy_setopt(handle, CURLOPT_SSL_SESSIONID_CACHE, 1L);
        if (cRet != CURLE_OK) DBGPRINTF("omhttp: curlSetupCommon unknown option CURLOPT_SSL_SESSIONID_CACHE\n");
    }
    /* uncomment for in-dept debuggung:
    curl_easy_setopt(handle, CURLOPT_VERBOSE, TRUE); */
}

static void ATTR_NONNULL() curlCheckConnSetup(wrkrInstanceData_t *const pWrkrData, serverData_t *serverData) {
    PTR_ASSERT_SET_TYPE(pWrkrData, WRKR_DATA_TYPE_ES);
    curlSetupCommon(pWrkrData, serverData, serverData->curlCheckConnHandle);
    curl_easy_setopt(serverData->curlCheckConnHandle, CURLOPT_HTTPHEADER, serverData->curlCheckHeader);
    curl_easy_setopt(serverData->curlCheckConnHandle, CURLOPT_TIMEOUT_MS, pWrkrData->pData->healthCheckTimeout);
}

static void ATTR_NONNULL(1) curlPostSetup(wrkrInstanceData_t *const pWrkrData, serverData_t *serverData) {
    PTR_ASSERT_SET_TYPE(pWrkrData, WRKR_DATA_TYPE_ES);
    curlSetupCommon(pWrkrData, serverData, serverData->curlPostHandle);
    curl_easy_setopt(serverData->curlPostHandle, CURLOPT_POST, 1L);
    CURLcode cRet;
    /* Enable TCP keep-alive for this transfer */
    cRet = curl_easy_setopt(serverData->curlPostHandle, CURLOPT_TCP_KEEPALIVE, 1L);
    if (cRet != CURLE_OK) DBGPRINTF("omhttp: curlPostSetup unknown option CURLOPT_TCP_KEEPALIVE\n");
    /* keep-alive idle time to 120 seconds */
    cRet = curl_easy_setopt(serverData->curlPostHandle, CURLOPT_TCP_KEEPIDLE, 120L);
    if (cRet != CURLE_OK) DBGPRINTF("omhttp: curlPostSetup unknown option CURLOPT_TCP_KEEPIDLE\n");
    /* interval time between keep-alive probes: 60 seconds */
    cRet = curl_easy_setopt(serverData->curlPostHandle, CURLOPT_TCP_KEEPINTVL, 60L);
    if (cRet != CURLE_OK) DBGPRINTF("omhttp: curlPostSetup unknown option CURLOPT_TCP_KEEPINTVL\n");

    cRet = curl_easy_setopt(serverData->curlPostHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    if (cRet != CURLE_OK) DBGPRINTF("omhttp: curlPostSetup unknown option CURLOPT_HTTP_VERSION\n");
}

static rsRetVal ATTR_NONNULL() curlSetup(wrkrInstanceData_t *const pWrkrData) {
    serverData_t *serverData = NULL;
    int i = 0;

    DEFiRet;
    for (i = 0; i < pWrkrData->pData->numServers; i++) {
        serverData = pWrkrData->listServerDataWkr[i];
        assert(serverData != NULL);
        CHKiRet(buildCurlHeaders(pWrkrData, serverData, &serverData->curlHeader, 0));
        CHKiRet(buildCurlHeaders(pWrkrData, serverData, &serverData->curlCheckHeader, 0));
        CHKmalloc(serverData->curlCheckConnHandle = curl_easy_init());
        CHKmalloc(serverData->curlPostHandle = curl_easy_init());
        curlPostSetup(pWrkrData, serverData);
        curlCheckConnSetup(pWrkrData, serverData);
    }
finalize_it:
    if (iRet != RS_RET_OK) {
        for (int j = 0; j < i; j++) {
            cleanupServerData(pWrkrData->listServerDataWkr[j]);
        }
        cleanupServerData(serverData);
    }
    RETiRet;
}


static void cleanupServerData(serverData_t *serverData) {
    if (serverData == NULL) {
        return;
    }
    curl_easy_cleanup(serverData->curlCheckConnHandle);
    serverData->curlCheckConnHandle = NULL;
    curl_easy_cleanup(serverData->curlPostHandle);
    serverData->curlPostHandle = NULL;
    free(serverData->fullUrlPost);
    serverData->fullUrlPost = NULL;
    free(serverData->fullUrlHealth);
    serverData->fullUrlHealth = NULL;
    free(serverData->restPATH);
    serverData->restPATH = NULL;
    curl_slist_free_all(serverData->curlHeader);
    serverData->curlHeader = NULL;
    curl_slist_free_all(serverData->curlCheckHeader);
    serverData->curlCheckHeader = NULL;
    serverData->gzipHeaderEnabled = 0;
}

static void freeServerData(serverData_t *serverData) {
    cleanupServerData(serverData);
    free(serverData);
}

static void freeWorkerServerData(wrkrInstanceData_t *pWrkrData) {
    if (pWrkrData->listServerDataWkr == NULL) {
        return;
    }

    for (int i = 0; i < pWrkrData->numServers; ++i) {
        freeServerData(pWrkrData->listServerDataWkr[i]);
        pWrkrData->listServerDataWkr[i] = NULL;
    }
}

static void ATTR_NONNULL() curlCleanup(wrkrInstanceData_t *const pWrkrData) {
    int size = pWrkrData->numServers;
    if (pWrkrData->listServerDataWkr != NULL) {
        for (int i = 0; i < size; i++) {
            cleanupServerData(pWrkrData->listServerDataWkr[i]);
        }
    }
}

static void ATTR_NONNULL() setInstParamDefaults(instanceData *const pData) {
    pData->serverBaseUrls = NULL;
    pData->defaultPort = 443;
    pData->healthCheckTimeout = 3500;
    pData->healthCheckTimeDelay = -1;  // in seconds | -1 is disable | HTTP_DEFAULT_HEALTHCHECKTIMEDELAY
    pData->token = NULL;
    pData->uid = NULL;
    pData->restPathTimeout = 30000;  // Set 30000ms (30s) timeout like health check
    pData->httpcontenttype = NULL;
    pData->headerContentTypeBuf = NULL;
    pData->httpheaderkey = NULL;
    pData->httpheadervalue = NULL;
    pData->httpHeaders = NULL;
    pData->nHttpHeaders = 0;
    pData->pwd = NULL;
    pData->authBuf = NULL;
    pData->headerBuf = NULL;
    pData->restPath = NULL;
    pData->checkPath = NULL;
    pData->dynRestPath = 0;
    pData->proxyHost = NULL;
    pData->proxyPort = 0;
    pData->batchMode = 0;
    pData->batchFormatName = (uchar *)"newline";
    pData->batchFormat = FMT_NEWLINE;
    pData->bFreeBatchFormatName = 0;
    pData->useHttps = 1;
    pData->maxBatchBytes = DEFAULT_MAX_BATCH_BYTES;  // 10 MB - default max message size for AWS API Gateway
    pData->maxBatchSize = 100;  // 100 messages
    pData->replyMaxBytes = DEFAULT_REPLY_MAX_BYTES;  // 1 MB default max response size (0 disables limit)
    pData->compress = 0;  // off
    pData->compressionLevel = -1;  // default compression
    pData->allowUnsignedCerts = 0;
    pData->skipVerifyHost = 0;
    pData->tplName = NULL;
    pData->errorFile = NULL;
    pData->caCertFile = NULL;
    pData->myCertFile = NULL;
    pData->myPrivKeyFile = NULL;
    pData->reloadOnHup = 0;
    pData->retryFailures = 0;
    pData->retryAddMetadata = 0;
    pData->nhttpRetryCodes = 0;
    pData->httpRetryCodes = NULL;
    pData->ratelimitBurst = -1;
    pData->ratelimitInterval = -1;
    pData->ratelimiter = NULL;
    pData->pszRatelimitName = NULL;
    pData->retryRulesetName = NULL;
    pData->retryRuleset = NULL;
    pData->nIgnorableCodes = 0;
    pData->ignorableCodes = NULL;
    pData->statsBySenders = 0;  // Disable by default
    // increment number of instances
    ++omhttpInstancesCnt;
}

static rsRetVal checkHeaderParam(char *const param) {
    DEFiRet;
    char *val = strstr(param, ":");
    if (val == NULL) {
        LogError(0, RS_RET_PARAM_ERROR,
                 "missing ':' delimiter in "
                 "parameter '%s'",
                 param);
        ABORT_FINALIZE(RS_RET_PARAM_ERROR);
    }
finalize_it:
    RETiRet;
}

/* Apply profile settings to instance configuration
 * Profiles are meta-configurations that set multiple parameters at once
 * for common use cases like Loki or Splunk HEC
 */
static rsRetVal applyProfileSettings(instanceData *const pData, const char *const profile) {
    DEFiRet;

    if (strcasecmp(profile, "loki") == 0) {
        /* Loki profile settings */
        LogMsg(0, RS_RET_OK, LOG_INFO, "omhttp: applying 'loki' profile");
        pData->vendor = LOKI;

        /* Set batch format to lokirest if not already set */
        if (!pData->bFreeBatchFormatName) {
            pData->batchFormatName = (uchar *)"lokirest";
            pData->batchFormat = FMT_LOKIREST;
        }

        /* Set default rest path for Loki if not set */
        if (pData->restPath == NULL) {
            CHKmalloc(pData->restPath = (uchar *)strdup("loki/api/v1/push"));
        }

        /* Enable batch mode by default for Loki */
        if (!pData->batchMode) {
            pData->batchMode = 1;
        }

        /* Enable compression for Loki (typically beneficial) */
        if (!pData->compress) {
            pData->compress = 1;
            pData->compressionLevel = -1; /* default compression */
        }

        /* Set default retry codes to 5xx if not configured */
        if (pData->nhttpRetryCodes == 0) {
            static const unsigned int loki_retry_codes[] = {500, 502, 503, 504};
            const size_t num_codes = sizeof(loki_retry_codes) / sizeof(loki_retry_codes[0]);
            pData->nhttpRetryCodes = num_codes;
            CHKmalloc(pData->httpRetryCodes = malloc(sizeof(loki_retry_codes)));
            memcpy(pData->httpRetryCodes, loki_retry_codes, sizeof(loki_retry_codes));
        }

    } else if (strncasecmp(profile, "hec:", 4) == 0) {
        /* HEC (HTTP Event Collector) profile */
        const char *vendor = profile + 4;

        if (strncasecmp(vendor, "splunk:", 7) == 0) {
            /*  Check endpoint */
            const char *endpoint = vendor + 7;
            pData->vendor = SPLUNK;

            if (strcasecmp(endpoint, "raw") == 0) {
                LogMsg(0, RS_RET_OK, LOG_INFO, "omhttp: applying 'hec:splunk:raw' profile");

                /* Set default rest path for Splunk HEC */
                if (pData->restPath == NULL && !pData->dynRestPath) {
                    CHKmalloc(pData->restPath = (uchar *)strdup(SPLUNK_HEC_RESTPATH_ENDPOINT_RAW));
                }

                /* Set batch format to newline (Splunk HEC uses newline-delimited format) */
                if (!pData->bFreeBatchFormatName) {
                    pData->batchFormatName = (uchar *)"newline";
                    pData->batchFormat = FMT_NEWLINE;
                }

                /* Set a custom template */
                if (pData->tplName == NULL) {
                    CHKmalloc(pData->tplName = ustrdup((uchar *)NAME_TPL_SPLK_RAW));
                }
            } else if (strcasecmp(endpoint, "event") == 0) {
                LogMsg(0, RS_RET_OK, LOG_INFO, "omhttp: applying 'hec:splunk:event' profile");

                /* Set default rest path for Splunk HEC */
                if (pData->restPath == NULL && !pData->dynRestPath) {
                    CHKmalloc(pData->restPath = (uchar *)strdup(SPLUNK_HEC_RESTPATH_ENDPOINT_EVENT));
                }

                /* Set batch format to newline (Splunk HEC uses newline-delimited format) */
                if (!pData->bFreeBatchFormatName) {
                    pData->batchFormatName = (uchar *)"newline";
                    pData->batchFormat = FMT_NEWLINE;
                }

                /* Enable batch mode for HEC */
                if (!pData->batchMode) {
                    pData->batchMode = 1;
                }

                /* Set a custom template */
                if (pData->tplName == NULL) {
                    CHKmalloc(pData->tplName = ustrdup((uchar *)NAME_TPL_SPLK_EVENT));
                }
            } else {
                LogError(0, RS_RET_PARAM_ERROR, "omhttp: unknown Splunk HEC endpoint '%s' in profile", endpoint);
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }
            /* Header */
            if (pData->token != NULL) {
                /* Create authorization header */
                es_str_t *tmpHeader = es_newStr(10240);
                int r = es_addBuf(&tmpHeader, "Authorization", strlen("Authorization"));
                if (r == 0) r = es_addChar(&tmpHeader, ':');
                if (r == 0) r = es_addChar(&tmpHeader, ' ');
                if (r == 0) r = es_addBuf(&tmpHeader, SPLUNK_HEC_HEADER_AUTH, strlen(SPLUNK_HEC_HEADER_AUTH));
                if (r == 0) r = es_addBuf(&tmpHeader, (char *)pData->token, ustrlen(pData->token));

                if (r != 0 || tmpHeader == NULL) {
                    LogError(0, RS_RET_ERR, "omhttp: error occurred computing token");
                    ABORT_FINALIZE(RS_RET_ERR);
                }
                // Correction of code
                if (pData->nHttpHeaders == 0) {
                    CHKmalloc(pData->httpHeaders = malloc(sizeof(uchar *) * 1));  // Only one HEADER
                    pData->nHttpHeaders = 1;
                    pData->httpHeaders[0] = (uchar *)es_str2cstr(tmpHeader, NULL);
                } else {
                    uchar **new_headers = realloc(pData->httpHeaders, sizeof(uchar *) * (pData->nHttpHeaders + 1));
                    if (new_headers == NULL) {
                        LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: error reallocating httpHeaders for auth token");
                        es_deleteStr(tmpHeader);
                        ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
                    }
                    pData->httpHeaders = new_headers;

                    pData->httpHeaders[pData->nHttpHeaders] = (uchar *)es_str2cstr(tmpHeader, NULL);
                    pData->nHttpHeaders += 1;
                }
                if (tmpHeader != NULL) es_deleteStr(tmpHeader);
            } else {
                LogError(0, RS_RET_PARAM_ERROR, "omhttp: Splunk HEC endpoint : a token is need it");
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }

            /* Set default max batch bytes (Splunk recommends < 1MB) */
            if (pData->maxBatchBytes == DEFAULT_MAX_BATCH_BYTES) { /* still default */
                pData->maxBatchBytes = SPLUNK_HEC_MAX_BATCH_BYTES; /* 1MB */
            }

            /* Set default retry codes to 5xx
             * https://docs.splunk.com/Documentation/Splunk/9.4.2/Data/TroubleshootHTTPEventCollector#Possible_error_codes
             */
            if (pData->nhttpRetryCodes == 0) {
                static const unsigned int splk_retry_codes[] = {500, 503};
                const size_t num_codes = sizeof(splk_retry_codes) / sizeof(splk_retry_codes[0]);
                pData->nhttpRetryCodes = num_codes;
                CHKmalloc(pData->httpRetryCodes = malloc(sizeof(splk_retry_codes)));
                memcpy(pData->httpRetryCodes, splk_retry_codes, sizeof(splk_retry_codes));
            }

            /* Define health Check URL */
            if (pData->checkPath == NULL) {
                CHKmalloc(pData->checkPath = (uchar *)strdup(SPLUNK_HEC_RESTPATH_ENDPOINT_HEALTH));
            }


        } else {
            LogError(0, RS_RET_PARAM_ERROR, "omhttp: unknown HEC vendor '%s' in profile", vendor);
            ABORT_FINALIZE(RS_RET_PARAM_ERROR);
        }

    } else {
        LogError(0, RS_RET_PARAM_ERROR, "omhttp: unknown profile '%s'", profile);
        ABORT_FINALIZE(RS_RET_PARAM_ERROR);
    }

finalize_it:
    RETiRet;
}


static rsRetVal setStatsObject(instanceData *const pData, const char *const serverParam, size_t index) {
    uchar ctrName[256];
    DEFiRet;

    targetStats_t *const serverStats = &pData->listObjStats[index];

    if (serverParam == NULL) {
        snprintf((char *)ctrName, sizeof(ctrName), "%s(ALL)", pData->statsName);
        ctrName[sizeof(ctrName) - 1] = '\0';
    } else {
        snprintf((char *)ctrName, sizeof(ctrName), "%s(%s)", pData->statsName, serverParam);
        ctrName[sizeof(ctrName) - 1] = '\0';
    }

    // instantiate the stats object and add the counters
    CHKiRet(statsobj.Construct(&(serverStats->defaultstats)));
    CHKiRet(statsobj.SetName(serverStats->defaultstats, ctrName));
    CHKiRet(statsobj.SetOrigin(serverStats->defaultstats, (uchar *)"omhttp"));

    STATSCOUNTER_INIT(serverStats->ctrMessagesSubmitted, serverStats->mutCtrMessagesSubmitted);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"messages.submitted", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrMessagesSubmitted)));

    STATSCOUNTER_INIT(serverStats->ctrMessagesSuccess, serverStats->mutCtrMessagesSuccess);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"messages.success", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrMessagesSuccess)));

    STATSCOUNTER_INIT(serverStats->ctrMessagesFail, serverStats->mutCtrMessagesFail);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"messages.fail", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrMessagesFail)));

    STATSCOUNTER_INIT(serverStats->ctrMessagesRetry, serverStats->mutCtrMessagesRetry);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"messages.retry", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrMessagesRetry)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestSuccess, serverStats->mutCtrHttpRequestSuccess);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"request.success", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestSuccess)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestFail, serverStats->mutCtrHttpRequestFail);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"request.fail", ctrType_IntCtr, CTR_FLAG_RESETTABLE,
                                &(serverStats->ctrHttpRequestFail)));

    STATSCOUNTER_INIT(serverStats->ctrHttpStatusSuccess, serverStats->mutCtrHttpStatusSuccess);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"request.status.success", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpStatusSuccess)));

    STATSCOUNTER_INIT(serverStats->ctrHttpStatusFail, serverStats->mutCtrHttpStatusFail);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"request.status.fail", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpStatusFail)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsCount, serverStats->mutCtrHttpRequestsCount);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.count", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsCount)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsStatus0xx, serverStats->mutCtrHttpRequestsStatus0xx);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.status.0xx", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsStatus0xx)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsStatus1xx, serverStats->mutCtrHttpRequestsStatus1xx);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.status.1xx", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsStatus1xx)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsStatus2xx, serverStats->mutCtrHttpRequestsStatus2xx);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.status.2xx", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsStatus2xx)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsStatus3xx, serverStats->mutCtrHttpRequestsStatus3xx);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.status.3xx", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsStatus3xx)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsStatus4xx, serverStats->mutCtrHttpRequestsStatus4xx);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.status.4xx", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsStatus4xx)));

    STATSCOUNTER_INIT(serverStats->ctrHttpRequestsStatus5xx, serverStats->mutCtrHttpRequestsStatus5xx);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.status.5xx", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->ctrHttpRequestsStatus5xx)));

    STATSCOUNTER_INIT(serverStats->httpRequestsBytes, serverStats->mutHttpRequestsBytes);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.bytes", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->httpRequestsBytes)));

    STATSCOUNTER_INIT(serverStats->httpRequestsTimeMs, serverStats->mutHttpRequestsTimeMs);
    CHKiRet(statsobj.AddCounter(serverStats->defaultstats, (uchar *)"requests.time_ms", ctrType_IntCtr,
                                CTR_FLAG_RESETTABLE, &(serverStats->httpRequestsTimeMs)));

    CHKiRet(statsobj.ConstructFinalize(serverStats->defaultstats));

finalize_it:
    RETiRet;
}

BEGINnewActInst
    struct cnfparamvals *pvals;
    char *serverParam = NULL;
    struct cnfarray *servers = NULL;
    char *profileName = NULL;
    int i;
    int iNumTpls;
    FILE *fp;
    char errStr[1024];
    char *batchFormatName;
    int compressionLevel = -1;
    CODESTARTnewActInst;
    if ((pvals = nvlstGetParams(lst, &actpblk, NULL)) == NULL) {
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    CHKiRet(createInstance(&pData));
    setInstParamDefaults(pData);

    for (i = 0; i < actpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(actpblk.descr[i].name, "server")) {
            servers = pvals[i].val.d.ar;
        } else if (!strcmp(actpblk.descr[i].name, "errorfile")) {
            CHKmalloc(pData->errorFile = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "serverport")) {
            pData->defaultPort = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "healthchecktimeout")) {
            pData->healthCheckTimeout = (long)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "healthchecktimedelay")) {
            pData->healthCheckTimeDelay = (long)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "restpathtimeout")) {
            pData->restPathTimeout = (long)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "token")) {
            CHKmalloc(pData->token = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "uid")) {
            CHKmalloc(pData->uid = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "httpcontenttype")) {
            CHKmalloc(pData->httpcontenttype = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "httpheaderkey")) {
            CHKmalloc(pData->httpheaderkey = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "httpheadervalue")) {
            CHKmalloc(pData->httpheadervalue = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "httpheaders")) {
            const int nmemb = pvals[i].val.d.ar->nmemb;
            if (nmemb < 0 || (size_t)nmemb > SIZE_MAX / sizeof(uchar *)) {
                LogError(0, RS_RET_PARAM_ERROR, "omhttp: too many httpheaders configured");
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }
            pData->nHttpHeaders = nmemb;
            CHKmalloc(pData->httpHeaders = malloc(sizeof(uchar *) * (size_t)nmemb));
            for (int j = 0; j < nmemb; ++j) {
                char *cstr = es_str2cstr(pvals[i].val.d.ar->arr[j], NULL);
                CHKiRet(checkHeaderParam(cstr));
                pData->httpHeaders[j] = (uchar *)cstr;
            }
        } else if (!strcmp(actpblk.descr[i].name, "pwd")) {
            CHKmalloc(pData->pwd = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "restpath")) {
            CHKmalloc(pData->restPath = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "checkpath")) {
            CHKmalloc(pData->checkPath = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "dynrestpath")) {
            pData->dynRestPath = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "proxyhost")) {
            CHKmalloc(pData->proxyHost = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "proxyport")) {
            pData->proxyPort = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "batch")) {
            pData->batchMode = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "batch.format")) {
            CHKmalloc(batchFormatName = es_str2cstr(pvals[i].val.d.estr, NULL));
            if (strstr(VALID_BATCH_FORMATS, batchFormatName) != NULL) {
                pData->batchFormatName = (uchar *)batchFormatName;
                pData->bFreeBatchFormatName = 1;
                if (!strcmp(batchFormatName, "newline")) {
                    pData->batchFormat = FMT_NEWLINE;
                } else if (!strcmp(batchFormatName, "jsonarray")) {
                    pData->batchFormat = FMT_JSONARRAY;
                } else if (!strcmp(batchFormatName, "kafkarest")) {
                    pData->batchFormat = FMT_KAFKAREST;
                } else if (!strcmp(batchFormatName, "lokirest")) {
                    pData->batchFormat = FMT_LOKIREST;
                }
            } else {
                LogError(0, NO_ERRCODE, "error: 'batch.format' %s unknown defaulting to 'newline'", batchFormatName);
            }
        } else if (!strcmp(actpblk.descr[i].name, "batch.maxbytes")) {
            pData->maxBatchBytes = (size_t)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "batch.maxsize")) {
            pData->maxBatchSize = (size_t)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "replymaxbytes")) {
            pData->replyMaxBytes = (size_t)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "compress")) {
            pData->compress = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "compress.level")) {
            compressionLevel = pvals[i].val.d.n;
            if (compressionLevel == -1 || (compressionLevel >= 0 && compressionLevel < 10)) {
                pData->compressionLevel = compressionLevel;
            } else {
                LogError(0, NO_ERRCODE,
                         "omhttp: invalid compress.level %d using default instead,"
                         "valid levels are -1 and 0-9",
                         compressionLevel);
            }
        } else if (!strcmp(actpblk.descr[i].name, "allowunsignedcerts")) {
            pData->allowUnsignedCerts = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "skipverifyhost")) {
            pData->skipVerifyHost = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "usehttps")) {
            pData->useHttps = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "template")) {
            CHKmalloc(pData->tplName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "tls.cacert")) {
            CHKmalloc(pData->caCertFile = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            fp = fopen((const char *)pData->caCertFile, "r");
            if (fp == NULL) {
                rs_strerror_r(errno, errStr, sizeof(errStr));
                LogError(0, RS_RET_NO_FILE_ACCESS, "error: 'tls.cacert' file %s couldn't be accessed: %s\n",
                         pData->caCertFile, errStr);
            } else {
                fclose(fp);
            }
        } else if (!strcmp(actpblk.descr[i].name, "tls.mycert")) {
            CHKmalloc(pData->myCertFile = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            fp = fopen((const char *)pData->myCertFile, "r");
            if (fp == NULL) {
                rs_strerror_r(errno, errStr, sizeof(errStr));
                LogError(0, RS_RET_NO_FILE_ACCESS, "error: 'tls.mycert' file %s couldn't be accessed: %s\n",
                         pData->myCertFile, errStr);
            } else {
                fclose(fp);
            }
        } else if (!strcmp(actpblk.descr[i].name, "tls.myprivkey")) {
            CHKmalloc(pData->myPrivKeyFile = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
            fp = fopen((const char *)pData->myPrivKeyFile, "r");
            if (fp == NULL) {
                rs_strerror_r(errno, errStr, sizeof(errStr));
                LogError(0, RS_RET_NO_FILE_ACCESS, "error: 'tls.myprivkey' file %s couldn't be accessed: %s\n",
                         pData->myPrivKeyFile, errStr);
            } else {
                fclose(fp);
            }
        } else if (!strcmp(actpblk.descr[i].name, "reloadonhup")) {
            pData->reloadOnHup = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "httpretrycodes")) {
            const int nmemb = pvals[i].val.d.ar->nmemb;
            if (nmemb < 0 || (size_t)nmemb > SIZE_MAX / sizeof(unsigned int)) {
                LogError(0, RS_RET_PARAM_ERROR, "omhttp: too many httpretrycodes configured");
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }
            pData->nhttpRetryCodes = nmemb;
            // note: use zero as sentinel value
            CHKmalloc(pData->httpRetryCodes = calloc((size_t)nmemb, sizeof(unsigned int)));
            int count = 0;
            for (int j = 0; j < nmemb; ++j) {
                int bSuccess = 0;
                long long n = es_str2num(pvals[i].val.d.ar->arr[j], &bSuccess);
                if (!bSuccess) {
                    char *cstr = es_str2cstr(pvals[i].val.d.ar->arr[j], NULL);
                    LogError(0, RS_RET_NO_FILE_ACCESS, "error: 'httpRetryCode' '%s' is not a number - ignored\n", cstr);
                    free(cstr);
                } else {
                    pData->httpRetryCodes[count++] = n;
                }
            }
        } else if (!strcmp(actpblk.descr[i].name, "retry")) {
            pData->retryFailures = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "retry.ruleset")) {
            CHKmalloc(pData->retryRulesetName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "retry.addmetadata")) {
            pData->retryAddMetadata = pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "ratelimit.burst")) {
            pData->ratelimitBurst = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "ratelimit.interval")) {
            pData->ratelimitInterval = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "ratelimit.name")) {
            CHKmalloc(pData->pszRatelimitName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "name")) {
            CHKmalloc(pData->statsName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "httpignorablecodes")) {
            const int nmemb = pvals[i].val.d.ar->nmemb;
            if (nmemb < 0 || (size_t)nmemb > SIZE_MAX / sizeof(unsigned int)) {
                LogError(0, RS_RET_PARAM_ERROR, "omhttp: too many httpignorablecodes configured");
                ABORT_FINALIZE(RS_RET_PARAM_ERROR);
            }
            pData->nIgnorableCodes = nmemb;
            // note: use zero as sentinel value
            CHKmalloc(pData->ignorableCodes = calloc((size_t)nmemb, sizeof(unsigned int)));
            int count = 0;
            for (int j = 0; j < nmemb; ++j) {
                int bSuccess = 0;
                long long n = es_str2num(pvals[i].val.d.ar->arr[j], &bSuccess);
                if (!bSuccess) {
                    char *cstr = es_str2cstr(pvals[i].val.d.ar->arr[j], NULL);
                    LogError(0, RS_RET_NO_FILE_ACCESS, "error: 'httpIgnorableCodes' '%s' is not a number - ignored\n",
                             cstr);
                    free(cstr);
                } else {
                    pData->ignorableCodes[count++] = n;
                }
            }
        } else if (!strcmp(actpblk.descr[i].name, "profile")) {
            CHKmalloc(profileName = es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(actpblk.descr[i].name, "statsbysenders")) {
            pData->statsBySenders = pvals[i].val.d.n;
        } else {
            LogError(0, RS_RET_INTERNAL_ERROR,
                     "omhttp: program error, "
                     "non-handled param '%s'",
                     actpblk.descr[i].name);
        }
    }

    if (pData->pszRatelimitName != NULL) {
        if (pData->ratelimitInterval != -1 || pData->ratelimitBurst != -1) {
            LogError(0, RS_RET_INVALID_PARAMS,
                     "omhttp: ratelimit.name is mutually exclusive with "
                     "ratelimit.interval and ratelimit.burst - using ratelimit.name");
        }
        pData->ratelimitInterval = 0;
        pData->ratelimitBurst = 0;
    } else {
        if (pData->ratelimitInterval == -1) pData->ratelimitInterval = 600;
        if (pData->ratelimitBurst == -1) pData->ratelimitBurst = 20000;
    }

    if (pData->pwd != NULL && pData->uid == NULL) {
        LogError(0, RS_RET_UID_MISSING,
                 "omhttp: password is provided, but no uid "
                 "- action definition invalid");
        ABORT_FINALIZE(RS_RET_UID_MISSING);
    }
    if (pData->httpheaderkey != NULL && pData->httpheadervalue == NULL) {
        LogError(0, RS_RET_UID_MISSING,
                 "omhttp: http header key is provided, but no http header value "
                 "- action definition invalid");
        ABORT_FINALIZE(RS_RET_UID_MISSING);
    }
    if (pData->dynRestPath && pData->restPath == NULL) {
        LogError(0, RS_RET_CONFIG_ERROR,
                 "omhttp: requested dynamic rest path, but no name for rest "
                 "path template given - action definition invalid");
        ABORT_FINALIZE(RS_RET_CONFIG_ERROR);
    }

    /* Apply header before profile */
    if (pData->httpcontenttype != NULL)
        CHKiRet(computeApiHeader((char *)"Content-Type", (char *)pData->httpcontenttype, &pData->headerContentTypeBuf));

    if (pData->httpheaderkey != NULL)
        CHKiRet(computeApiHeader((char *)pData->httpheaderkey, (char *)pData->httpheadervalue, &pData->headerBuf));


    /* Apply profile settings if specified */
    if (profileName != NULL) {
        CHKiRet(applyProfileSettings(pData, profileName));
        free(profileName);
        profileName = NULL;
    }

    if (pData->proxyHost == NULL) {
        const char *http_proxy;
        if ((http_proxy = getenv("http_proxy")) == NULL) {
            http_proxy = getenv("HTTP_PROXY");
        }
        if (http_proxy != NULL) {
            pData->proxyHost = ustrdup(http_proxy);
        }
    }

    if (pData->uid != NULL) CHKiRet(computeAuthHeader((char *)pData->uid, (char *)pData->pwd, &pData->authBuf));

    iNumTpls = 1;
    if (pData->dynRestPath) ++iNumTpls;
    DBGPRINTF("omhttp: requesting %d templates\n", iNumTpls);
    CODE_STD_STRING_REQUESTnewActInst(iNumTpls);

    CHKiRet(OMSRsetEntry(*ppOMSR, 0, (uchar *)strdup((pData->tplName == NULL) ? " StdJSONFmt" : (char *)pData->tplName),
                         OMSR_NO_RQD_TPL_OPTS));


    /* we need to request additional templates. If we have a dynamic search index,
     * it will always be string 1. Type may be 1 or 2, depending on whether search
     * index is dynamic as well. Rule needs to be followed throughout the module.
     */
    iNumTpls = 1;
    if (pData->dynRestPath) {
        CHKiRet(OMSRsetEntry(*ppOMSR, iNumTpls, ustrdup(pData->restPath), OMSR_NO_RQD_TPL_OPTS));
        ++iNumTpls;
    }

    if (!pData->statsName) {
        uchar pszAName[64];
        snprintf((char *)pszAName, sizeof(pszAName), "omhttp-%d", omhttpInstancesCnt);
        pData->statsName = ustrdup(pszAName);
    }

    if (servers != NULL) {
        const int nmemb = servers->nmemb;
        if (nmemb <= 0 || (size_t)nmemb > SIZE_MAX / sizeof(uchar *) ||
            (pData->statsBySenders && (size_t)nmemb > SIZE_MAX / sizeof(targetStats_t)) ||
            (size_t)nmemb > SIZE_MAX / sizeof(sbool) || (size_t)nmemb > SIZE_MAX / sizeof(time_t)) {
            LogError(0, RS_RET_PARAM_ERROR, "omhttp: too many servers configured");
            ABORT_FINALIZE(RS_RET_PARAM_ERROR);
        }
        pData->numServers = nmemb;
        pData->serverBaseUrls = malloc((size_t)nmemb * sizeof(uchar *));
        if (pData->serverBaseUrls == NULL) {
            LogError(0, RS_RET_ERR,
                     "omhttp: unable to allocate buffer "
                     "for http server configuration.");
            ABORT_FINALIZE(RS_RET_ERR);
        }

        /* Set up the stats object array */
        const int numStats = pData->statsBySenders ? nmemb : 1;
        pData->listObjStats = malloc((size_t)numStats * sizeof(targetStats_t));
        if (pData->listObjStats == NULL) {
            LogError(0, RS_RET_ERR,
                     "omhttp: unable to allocate buffer "
                     "for http server stats object.");
            ABORT_FINALIZE(RS_RET_ERR);
        }

        if (!pData->statsBySenders) {
            CHKiRet(setStatsObject(pData, NULL, 0));
        }

        for (i = 0; i < servers->nmemb; ++i) {
            serverParam = es_str2cstr(servers->arr[i], NULL);
            if (serverParam == NULL) {
                LogError(0, RS_RET_ERR,
                         "omhttp: unable to allocate buffer "
                         "for http server configuration.");
                ABORT_FINALIZE(RS_RET_ERR);
            }
            /* Remove a trailing slash if it exists */
            const size_t serverParamLastChar = strlen(serverParam) - 1;
            if (serverParam[serverParamLastChar] == '/') {
                serverParam[serverParamLastChar] = '\0';
            }
            if (pData->statsBySenders) {
                CHKiRet(setStatsObject(pData, serverParam, i));
            }

            CHKiRet(computeBaseUrl(serverParam, pData->defaultPort, pData->useHttps, pData->serverBaseUrls + i));
            free(serverParam);
            serverParam = NULL;
        }
    } else {
        LogMsg(0, RS_RET_OK, LOG_WARNING, "omhttp: No servers specified, using localhost");
        pData->numServers = 1;
        pData->serverBaseUrls = malloc(sizeof(uchar *));
        if (pData->serverBaseUrls == NULL) {
            LogError(0, RS_RET_ERR,
                     "omhttp: unable to allocate buffer "
                     "for http server configuration.");
            ABORT_FINALIZE(RS_RET_ERR);
        }
        /* Set up the stats object array (force global stats) */
        pData->listObjStats = malloc(sizeof(targetStats_t));
        if (pData->listObjStats == NULL) {
            LogError(0, RS_RET_ERR,
                     "omhttp: unable to allocate buffer "
                     "for http server stats object.");
            ABORT_FINALIZE(RS_RET_ERR);
        }
        CHKiRet(setStatsObject(pData, NULL, 0));

        CHKiRet(computeBaseUrl("localhost", pData->defaultPort, pData->useHttps, pData->serverBaseUrls));
    }

    /* Alloc for checkConn */
    pData->serverSuspended = calloc(pData->numServers, sizeof(sbool));
    pData->serverNextRetry = calloc(pData->numServers, sizeof(time_t));
    pData->serverLastCheck = calloc(pData->numServers, sizeof(time_t));
    pData->serverCheckInProgress = calloc(pData->numServers, sizeof(sbool));

    if (pData->serverSuspended == NULL || pData->serverNextRetry == NULL || pData->serverLastCheck == NULL ||
        pData->serverCheckInProgress == NULL) {
        LogError(0, RS_RET_OUT_OF_MEMORY, "omhttp: failed to allocate global state arrays");
        ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
    }

    if (pData->retryFailures) {
        if (pData->pszRatelimitName != NULL) {
            CHKiRet(ratelimitNewFromConfig(&pData->ratelimiter, loadModConf->pConf, (char *)pData->pszRatelimitName,
                                           "omhttp", NULL));
        } else {
            CHKiRet(ratelimitNew(&pData->ratelimiter, "omhttp", NULL));
            ratelimitSetLinuxLike(pData->ratelimiter, (unsigned)pData->ratelimitInterval,
                                  (unsigned)pData->ratelimitBurst);
        }
        ratelimitSetNoTimeCache(pData->ratelimiter);
    }


    /* node created, let's add to list of instance configs for the module */
    if (loadModConf->tail == NULL) {
        loadModConf->tail = loadModConf->root = pData;
    } else {
        loadModConf->tail->next = pData;
        loadModConf->tail = pData;
    }

    CODE_STD_FINALIZERnewActInst;
    cnfparamvalsDestruct(pvals, &actpblk);
    if (serverParam) free(serverParam);
    if (profileName) free(profileName);
ENDnewActInst


BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    loadModConf = pModConf;
    pModConf->pConf = pConf;
    pModConf->root = pModConf->tail = NULL;
ENDbeginCnfLoad


BEGINendCnfLoad
    CODESTARTendCnfLoad;
    loadModConf = NULL; /* done loading */
ENDendCnfLoad


BEGINcheckCnf
    instanceConf_t *inst;
    CODESTARTcheckCnf;
    for (inst = pModConf->root; inst != NULL; inst = inst->next) {
        ruleset_t *pRuleset;
        rsRetVal localRet;

        if (inst->retryRulesetName) {
            localRet = ruleset.GetRuleset(pModConf->pConf, &pRuleset, inst->retryRulesetName);
            if (localRet == RS_RET_NOT_FOUND) {
                LogError(0, localRet,
                         "omhttp: retry.ruleset '%s' not found - "
                         "no retry ruleset will be used",
                         inst->retryRulesetName);
            } else {
                inst->retryRuleset = pRuleset;
            }
        }
    }
ENDcheckCnf


BEGINactivateCnf
    CODESTARTactivateCnf;
ENDactivateCnf


BEGINfreeCnf
    CODESTARTfreeCnf;
ENDfreeCnf


// HUP handling for the instance...
BEGINdoHUP
    CODESTARTdoHUP;
    pthread_mutex_lock(&pData->mutErrFile);
    if (pData->fdErrFile != -1) {
        close(pData->fdErrFile);
        pData->fdErrFile = -1;
    }
    pthread_mutex_unlock(&pData->mutErrFile);
ENDdoHUP


// HUP handling for the worker...
BEGINdoHUPWrkr
    CODESTARTdoHUPWrkr;
    if (pWrkrData->pData->reloadOnHup) {
        LogMsg(0, NO_ERRCODE, LOG_INFO, "omhttp: received HUP reloading curl handles");
        curlCleanup(pWrkrData);
        CHKiRet(curlSetup(pWrkrData));
    }
finalize_it:
ENDdoHUPWrkr


BEGINmodExit
    CODESTARTmodExit;
    if (pInputName != NULL) prop.Destruct(&pInputName);
    curl_global_cleanup();
    objRelease(prop, CORE_COMPONENT);
    objRelease(ruleset, CORE_COMPONENT);
    objRelease(statsobj, CORE_COMPONENT);
ENDmodExit

NO_LEGACY_CONF_parseSelectorAct

    BEGINqueryEtryPt CODESTARTqueryEtryPt;
CODEqueryEtryPt_STD_OMODTX_QUERIES CODEqueryEtryPt_STD_OMOD8_QUERIES CODEqueryEtryPt_STD_CONF2_OMOD_QUERIES
    CODEqueryEtryPt_doHUP CODEqueryEtryPt_doHUPWrkr /* Load the worker HUP handling code */
    CODEqueryEtryPt_STD_CONF2_QUERIES
ENDqueryEtryPt


BEGINmodInit()
    CODESTARTmodInit;
    uchar *pTmp;
    *ipIFVersProvided = CURR_MOD_IF_VERSION; /* we only support the current interface specification */
    CODEmodInit_QueryRegCFSLineHdlr CHKiRet(objUse(prop, CORE_COMPONENT));
    CHKiRet(objUse(ruleset, CORE_COMPONENT));
    CHKiRet(objUse(statsobj, CORE_COMPONENT));

    if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
        LogError(0, RS_RET_OBJ_CREATION_FAILED, "CURL fail. -http disabled");
        ABORT_FINALIZE(RS_RET_OBJ_CREATION_FAILED);
    }

    /* Add custom template for Splunk endpoit RAW */
    pTmp = template_StdSplkRaw;
    tplAddLine(ourConf, (char *)NAME_TPL_SPLK_RAW, &pTmp);

    /* Add custom template for Splunk endpoit EVENT */
    pTmp = template_StdSplkEvent;
    tplAddLine(ourConf, (char *)NAME_TPL_SPLK_EVENT, &pTmp);

    CHKiRet(prop.Construct(&pInputName));
    CHKiRet(prop.SetString(pInputName, UCHAR_CONSTANT("omhttp"), sizeof("omhttp") - 1));
    CHKiRet(prop.ConstructFinalize(pInputName));
ENDmodInit

/* vi:set ai:
 */
