6#define _POSIX_C_SOURCE 200809L
46static int grpc_register_route(
cwist_app *app,
const char *service,
const char *method,
84static void grpc_session_send_trailers(cwist_grpc_session *session,
cwist_grpc_status_t status,
92static int grpc_content_type_is_grpc(
const char *content_type) {
93 if (!content_type)
return 0;
94 return strncmp(content_type,
"application/grpc", 16) == 0;
103 if (!app || !path)
return NULL;
127 if (!req || !key)
return NULL;
129 if (h->key && h->key->data && h->value && h->value->data &&
131 return h->value->data;
147 if (!value || !
out_ms)
return -1;
148 size_t len =
strlen(value);
150 char unit = value[len - 1];
168 for (
size_t i = 0;
i + 1 < len;
i++) {
169 if (!
isdigit((
unsigned char)value[
i]))
return -1;
174 if (
v > 0 &&
ms == 0)
ms = 1;
185 const char *
enc = grpc_header_get(req,
"grpc-encoding");
199 const char *
ae = grpc_header_get(req,
"grpc-accept-encoding");
202 while (*
ae ==
' ' || *
ae ==
'\t' || *
ae ==
',')
ae++;
205 while (*
ae && *
ae !=
',' && *
ae !=
' ' && *
ae !=
'\t')
ae++;
243 *out_len =
cap -
zs.avail_out;
264 if (
cap < 8192)
cap = 8192;
285 zs.next_out = buf + used;
288 used =
cap -
zs.avail_out;
309 if (grpc_request_encoding(req) != 1)
return -1;
324static int grpc_b64_val(
int c) {
325 if (
c >=
'A' &&
c <=
'Z')
return c -
'A';
326 if (
c >=
'a' &&
c <=
'z')
return c -
'a' + 26;
327 if (
c >=
'0' &&
c <=
'9')
return c -
'0' + 52;
328 if (
c ==
'+')
return 62;
329 if (
c ==
'/')
return 63;
345static int grpc_base64_decode(
const char *
in,
size_t in_len,
uint8_t *out,
size_t out_cap,
351 if (
in[
i] ==
'=')
break;
352 int v = grpc_b64_val((
unsigned char)
in[
i]);
353 if (
v < 0)
return -1;
358 if (used >= out_cap)
return -2;
372 return grpc_header_get(req, key);
386 size_t out_cap,
size_t *out_len) {
387 if (!req || !key || !out_len)
return -1;
390 const char *value = grpc_header_get(req, key);
391 if (!value)
return -1;
392 return grpc_base64_decode(value,
strlen(value), out, out_cap, out_len);
406 if (!req || !res || !req->
app || !req->
path || !req->
path->
data)
return;
415 if (!grpc_content_type_is_grpc(
ct)) {
418 if (!grpc_content_type_is_grpc(
ct)) {
420 "content-type must be application/grpc");
434 if (grpc_request_encoding(req) < 0) {
439 if (grpc_message_inflate(req, &message, &
inflated) != 0) {
444 route->handler(req, res, &message,
route->user_ctx);
447 if (grpc_client_accepts_gzip(req) && res->
body && res->
body->
size >= 5) {
477 if (!grpc_content_type_is_grpc(
ct)) {
480 if (!grpc_content_type_is_grpc(
ct)) {
482 "content-type must be application/grpc");
489 if (grpc_request_encoding(req) < 0) {
508 if (!req || !res || !req->
app || !req->
path || !req->
path->
data)
return;
515 if (grpc_validate_request(req, res) != 0)
return;
523 "failed to allocate gRPC stream messages");
535 "failed to grow gRPC stream messages");
541 &messages[
count]) != 0) {
553 if (messages[
i].compressed) {
562 "failed to allocate inflate state");
566 for (
size_t i = 0;
i <
count;
i++) {
567 if (grpc_message_inflate(req, &messages[
i], &
owned[
i]) != 0) {
571 "unsupported compressed gRPC message");
578 const char *
timeout = grpc_header_get(req,
"grpc-timeout");
582 deadline_ms = grpc_now_ms() + timeout_ms;
588 if (grpc_client_accepts_gzip(req)) {
596 .messages = messages,
597 .message_count =
count,
599 .status_message =
NULL,
603 .deadline_ms = deadline_ms,
605 route->stream_handler(&stream,
route->user_ctx);
627 if ((
size_t)len >
frame_len - 5)
return -1;
628 if ((
size_t)len !=
frame_len - 5)
return -1;
652 size_t pos = *offset;
658 if ((
size_t)len >
frames_len - pos - 5)
return -1;
663 *offset = pos + 5 + (
size_t)len;
678 uint8_t **out,
size_t *out_len) {
679 if (!out || !out_len || (payload_len > 0 && !payload))
return -1;
684 buf[0] = compressed ? 1 : 0;
685 buf[1] = (
uint8_t)((payload_len >> 24) & 0xff);
686 buf[2] = (
uint8_t)((payload_len >> 16) & 0xff);
687 buf[3] = (
uint8_t)((payload_len >> 8) & 0xff);
688 buf[4] = (
uint8_t)(payload_len & 0xff);
689 if (payload_len > 0)
memcpy(buf + 5, payload, payload_len);
692 *out_len = payload_len + 5;
702 if (!decoder)
return;
703 memset(decoder, 0,
sizeof(*decoder));
704 decoder->
max_message_size = max_message_size ? max_message_size : (16u * 1024u * 1024u);
714 if (!decoder)
return;
716 memset(decoder, 0,
sizeof(*decoder));
736 if (!decoder || (!data && len) || !
callback)
return -1;
755 if (!decoder->
payload)
return -1;
766 if (
callback(ctx, &message) != 0)
return -1;
785 const char *message,
const void *payload,
size_t payload_len) {
820 const char *message) {
837 if (!stream)
return -1;
841 }
else if (stream->
req) {
850 if (grpc_gzip_deflate(payload, payload_len, &
cbuf, &
clen) == 0) {
917 int (*write_frame)(
void *,
const uint8_t *,
size_t,
int),
934 const char *message) {
936 if (stream->
closed)
return;
942 grpc_session_send_trailers(stream->
session, status, message);
945 if (!stream->
res)
return;
984static int grpc_health_status_locked(
const cwist_grpc_health_registry *
reg,
const char *service) {
985 for (
const cwist_grpc_health_state *
it =
reg->states;
it;
it =
it->next)
986 if (
it->service &&
strcmp(
it->service, service ? service :
"") == 0)
988 return service && *service ? 3 : 1;
1019 char *service = grpc_health_service_copy(message);
1021 int status = grpc_health_status_locked(
reg, service);
1044 grpc_health_reply(res, ctx, message);
1056 const char *service) {
1058 int status = grpc_health_status_locked(
reg, service);
1082 char *service =
NULL;
1089 if (
rc > 0) service = grpc_health_service_copy(&
msg);
1093 service = grpc_health_service_copy(message);
1097 if (grpc_health_send_status(stream,
reg, service) != 0)
goto out;
1098 if (!stream->
session)
goto out;
1111 ts.tv_nsec += 100000000L;
1112 if (
ts.tv_nsec >= 1000000000L) {
1114 ts.tv_nsec -= 1000000000L;
1121 if (grpc_health_send_status(stream,
reg, service) != 0)
break;
1138 if (!app)
return -1;
1140 if (!
reg)
return -1;
1145 if (grpc_register_route(app,
"grpc.health.v1.Health",
"Check", 0, grpc_health_check,
NULL,
1147 grpc_register_route(app,
"grpc.health.v1.Health",
"Watch", 1,
NULL, grpc_health_watch,
1156 route = grpc_find_route(app,
"/grpc.health.v1.Health/Watch");
1175 if (!
route || !
route->user_ctx || !service)
return -1;
1179 if (
it->service &&
strcmp(
it->service, service) == 0) {
1180 it->serving = !!serving;
1192 if (!
item->service) {
1198 item->serving = !!serving;
1253 if (!
route->path ||
route->path[0] !=
'/')
continue;
1255 if (!
slash)
continue;
1258 char *service = cwist_scratch_alloc(&
service_s, len + 1);
1264 service[len] =
'\0';
1265 if (grpc_reflection_append_service(&
response, service) != 0)
1282 if (grpc_register_route(app,
"grpc.reflection.v1alpha.ServerReflection",
"ServerReflectionInfo",
1283 1,
NULL, grpc_reflection_info,
NULL) != 0)
1286 grpc_find_route(app,
"/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo");
1307static int grpc_register_route(
cwist_app *app,
const char *service,
const char *method,
1310 if (!app || !service || !method)
return -1;
1311 if ((!streaming && !
unary_handler) || (streaming && !stream_handler))
return -1;
1318 if (!path)
return -1;
1319 snprintf(path, path_len,
"/%s/%s", service, method);
1325 existing->stream_handler = stream_handler;
1337 route->streaming = streaming;
1339 route->stream_handler = stream_handler;
1340 route->user_ctx = user_ctx;
1344 cwist_app_post(app, path, streaming ? grpc_dispatch_stream : grpc_dispatch_unary);
1359 return grpc_register_route(app, service, method, 0, handler,
NULL, user_ctx);
1373 return grpc_register_route(app, service, method, 1,
NULL, handler, user_ctx);
1387 if (
route->builtin == 1 &&
route->user_ctx) {
1418 if (!
dst || !src)
return -1;
1421 if (
route->builtin == 1) {
1431 if (
route->builtin == 2) {
1435 if (
route->builtin == 3) {
1440 const char *path =
route->path;
1441 if (!path || path[0] !=
'/')
return -1;
1447 if (!service)
return -1;
1455 if (
rc != 0)
return -1;
1477static void grpc_conn_ctx_release(cwist_grpc_h2_conn_ctx *ctx) {
1480 last = (--ctx->refs == 0);
1495static void grpc_session_release(cwist_grpc_session *session) {
1500 while (*
pp && *
pp != session)
pp = &(*pp)->next;
1501 if (*
pp) *
pp = session->next;
1503 grpc_conn_ctx_release(ctx);
1506 while (session->qhead) {
1510 session->qhead = next;
1530static int grpc_session_push(cwist_grpc_session *session,
uint8_t *data,
size_t len) {
1532 if (!node)
return -1;
1537 if (session->cancelled || session->eof) {
1544 session->qtail->next = node;
1546 session->qhead = node;
1547 session->qtail = node;
1568 if (session->encoding != 1)
return -1;
1569 if (grpc_gzip_inflate(message->
data, message->
len, ©, &len) != 0)
return -1;
1572 if (!copy)
return -1;
1576 if (grpc_session_push(session, copy, len) != 0)
return -1;
1590 const char *message) {
1592 session->cancelled = 1;
1594 session->stream.status = status;
1595 session->stream.status_message = message;
1598 grpc_session_send_trailers(session, status, message);
1610static int grpc_session_send_headers_locked(cwist_grpc_session *session) {
1611 if (session->headers_sent)
return 0;
1612 if (!session->h2s)
return -1;
1619 session->headers_sent = 1;
1634static void grpc_session_send_trailers(cwist_grpc_session *session,
cwist_grpc_status_t status,
1635 const char *message) {
1639 if (session->stream.retry_pushback_set)
1653 if (session->stream.retry_pushback_set)
1656 session->headers_sent = 1;
1658 }
else if (grpc_session_send_headers_locked(session) == 0) {
1669 if (session->stream.retry_pushback_set) {
1670 pairs[
count].name =
"grpc-retry-pushback-ms";
1702 grpc_session_send_headers_locked(session) == 0)
1716static void *grpc_session_thread(
void *arg) {
1718 session->handler(&session->stream, session->user_ctx);
1719 if (!session->stream.closed) {
1721 session->stream.status_message);
1723 grpc_session_release(session);
1741 if (!stream || !out)
return -1;
1757 if (session->qhead) {
1759 session->qhead = node->next;
1760 if (!session->qhead) session->qtail =
NULL;
1763 session->recv_buf = node->data;
1765 out->
data = node->data;
1766 out->
len = node->len;
1770 if (session->cancelled) {
1781 session->cancelled = 1;
1795 ts.tv_nsec =
rt.tv_nsec + (
long)((
left % 1000) * 1000000);
1796 if (
ts.tv_nsec >= 1000000000L) {
1798 ts.tv_nsec -= 1000000000L;
1815 if (!stream)
return 0;
1822 int cancelled = session->cancelled;
1847static void *grpc_h2_on_conn_open(
void *user_ctx) {
1849 if (!ctx)
return NULL;
1852 ctx->sessions =
NULL;
1860static void grpc_h2_on_conn_close(
void *conn_ctx) {
1861 if (conn_ctx) grpc_conn_ctx_release(conn_ctx);
1878static void *grpc_h2_on_headers(
void *conn_ctx,
cwist_http_request *req, cwist_h2_stream *stream) {
1880 if (!ctx || !ctx->app || !req || !req->
path || !req->
path->
data)
return NULL;
1887 const char *
ct = grpc_header_get(req,
"content-type");
1888 if (!grpc_content_type_is_grpc(
ct))
return NULL;
1890 int encoding = grpc_request_encoding(req);
1891 if (encoding < 0)
return NULL;
1894 if (!session)
return NULL;
1895 memset(session, 0,
sizeof(*session));
1899 session->h2s = stream;
1900 session->conn_ctx = ctx;
1901 session->encoding = encoding;
1905 session->stream.req = req;
1906 session->stream.res =
NULL;
1907 session->stream.messages =
NULL;
1908 session->stream.message_count = 0;
1910 session->stream.status_message =
NULL;
1911 session->stream.closed = 0;
1912 session->stream.write_frame = grpc_session_write_frame;
1913 session->stream.write_frame_ctx = session;
1914 session->stream.session = session;
1916 session->stream.recv_pos = 0;
1917 const char *
timeout = grpc_header_get(req,
"grpc-timeout");
1921 session->stream.deadline_ms = grpc_now_ms() + timeout_ms;
1923 session->handler =
route->stream_handler;
1924 session->user_ctx =
route->user_ctx;
1925 session->resp_encoding = grpc_client_accepts_gzip(req) ? 1 : 0;
1933 session->next = ctx->sessions;
1934 ctx->sessions = session;
1942 while (*
pp && *
pp != session)
pp = &(*pp)->next;
1943 if (*
pp) *
pp = session->next;
1971static int grpc_h2_on_data(
void *conn_ctx,
void *
stream_ctx,
const unsigned char *data,
size_t len,
1975 if (!session)
return 1;
1977 if (data && len > 0) {
1980 if (session->encoding != 1) {
1983 "compressed gRPC message without grpc-encoding: gzip");
1986 "malformed gRPC message frame");
2007static void grpc_h2_on_cancel(
void *conn_ctx,
void *
stream_ctx) {
2010 if (!session)
return;
2012 session->cancelled = 1;
2028static int grpc_h2_on_poll(
void *conn_ctx,
void *
stream_ctx) {
2031 if (!session)
return 1;
2035 if (session->stream.deadline_ms &&
2037 grpc_now_ms() >= session->stream.deadline_ms) {
2039 session->cancelled = 1;
2056static uint64_t grpc_h2_next_deadline_ms(
void *conn_ctx) {
2061 for (cwist_grpc_session *s = ctx->sessions; s; s = s->next) {
2078static void grpc_h2_on_close(
void *conn_ctx,
void *
stream_ctx) {
2081 if (!session)
return;
2084 session->h2s =
NULL;
2087 session->cancelled = 1;
2091 grpc_session_release(session);
2096 .on_conn_close = grpc_h2_on_conn_close,
2097 .on_headers = grpc_h2_on_headers,
2098 .on_data = grpc_h2_on_data,
2099 .on_cancel = grpc_h2_on_cancel,
2100 .on_poll = grpc_h2_on_poll,
2101 .next_deadline_ms = grpc_h2_next_deadline_ms,
2102 .on_close = grpc_h2_on_close,
2110 return &grpc_h2_hooks;
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
#define CWIST_DEFER_FREE
Attach to a pointer variable's declaration to cwist_free() it automatically when that variable's encl...
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
#define CWIST_SCRATCH_DEFER
Attach to a cwist_scratch_t variable's declaration so its allocation (arena- or heap-backed,...
cwist_error_t cwist_sstring_append_len(cwist_sstring *str, const char *data, size_t len)
Append raw bytes with length.
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
struct cwist_grpc_session cwist_grpc_session
void cwist_grpc_set_response(cwist_http_response *res, cwist_grpc_status_t status, const char *message, const void *payload, size_t payload_len)
Build a complete buffered unary gRPC response.
int cwist_app_grpc_health_watchers(cwist_app *app)
Count currently active Health/Watch streams.
int cwist_grpc_decoder_feed(cwist_grpc_decoder *decoder, const void *data, size_t len, cwist_grpc_message_callback callback, void *ctx)
Feed bytes into an incremental gRPC message decoder.
int cwist_grpc_metadata_get_binary(cwist_http_request *req, const char *key, uint8_t *out, size_t out_cap, size_t *out_len)
Decode a binary ("-bin" suffixed) metadata header from base64.
int cwist_grpc_stream_recv(cwist_grpc_stream *stream, cwist_grpc_message *out)
Receive the next inbound message on a stream.
int cwist_grpc_parse_timeout(const char *value, uint64_t *out_ms)
Parse a gRPC timeout value into milliseconds.
struct grpc_qnode grpc_qnode
int cwist_app_grpc_health_set_status(cwist_app *app, const char *service, int serving)
Set or update the serving status of a named service.
struct cwist_grpc_route cwist_grpc_route
struct cwist_grpc_health_state cwist_grpc_health_state
struct cwist_grpc_h2_conn_ctx cwist_grpc_h2_conn_ctx
int cwist_app_grpc_stream(cwist_app *app, const char *service, const char *method, cwist_grpc_stream_handler_func handler, void *user_ctx)
Register a client-streaming gRPC method on an app.
void cwist_grpc_set_error(cwist_http_response *res, cwist_grpc_status_t status, const char *message)
Build a buffered gRPC error response with an empty message.
int cwist_grpc_stream_cancelled(cwist_grpc_stream *stream)
Check whether a stream has been cancelled.
const char * cwist_grpc_metadata_get(cwist_http_request *req, const char *key)
Fetch a metadata header value from a gRPC request.
void cwist_grpc_routes_destroy(cwist_app *app)
Free all gRPC routes and per-route resources on an app.
void cwist_grpc_decoder_init(cwist_grpc_decoder *decoder, size_t max_message_size)
Initialize an incremental gRPC message decoder.
int cwist_grpc_stream_send(cwist_grpc_stream *stream, const void *payload, size_t payload_len)
Send one message on an open gRPC stream.
void cwist_grpc_stream_set_retry_pushback(cwist_grpc_stream *stream, int32_t ms)
Advertise a server-side retry pushback hint to the client.
void cwist_grpc_decoder_destroy(cwist_grpc_decoder *decoder)
Release all resources held by a decoder and reset it to zero.
void cwist_grpc_stream_set_writer(cwist_grpc_stream *stream, int(*write_frame)(void *, const uint8_t *, size_t, int), void *ctx)
Install a custom frame writer on a stream.
int cwist_grpc_encode_message(const void *payload, size_t payload_len, uint8_t compressed, uint8_t **out, size_t *out_len)
Wrap a payload in a gRPC length-prefixed message frame.
int cwist_grpc_routes_clone(cwist_app *dst, const cwist_app *src)
Copy all gRPC routes (and built-in service state) to another app.
uint64_t cwist_grpc_stream_deadline_remaining_ms(cwist_grpc_stream *stream)
Milliseconds remaining until the stream deadline.
int cwist_app_grpc_unary(cwist_app *app, const char *service, const char *method, cwist_grpc_unary_handler_func handler, void *user_ctx)
Register a unary gRPC method on an app.
void cwist_grpc_stream_close(cwist_grpc_stream *stream, cwist_grpc_status_t status, const char *message)
Close a gRPC stream with a final status.
struct cwist_grpc_health_registry cwist_grpc_health_registry
int cwist_app_grpc_reflection(cwist_app *app)
Register the built-in gRPC server reflection service on an app.
int cwist_grpc_decode_message(const void *frame, size_t frame_len, cwist_grpc_message *out)
Decode a single gRPC length-prefixed message frame.
int cwist_grpc_decode_next_message(const void *frames, size_t frames_len, size_t *offset, cwist_grpc_message *out)
Decode the next message in a concatenated gRPC frame buffer.
int cwist_app_grpc_health(cwist_app *app)
Register the built-in grpc.health.v1.Health service on an app.
const cwist_http2_stream_hooks * cwist_grpc_http2_hooks(void)
Access the gRPC HTTP/2 stream hooks table.
gRPC over HTTP/2 helpers for CWIST.
@ CWIST_GRPC_RESOURCE_EXHAUSTED
@ CWIST_GRPC_INVALID_ARGUMENT
@ CWIST_GRPC_UNIMPLEMENTED
@ CWIST_GRPC_DEADLINE_EXCEEDED
int(* cwist_grpc_message_callback)(void *ctx, const cwist_grpc_message *message)
enum cwist_grpc_status cwist_grpc_status_t
void(* cwist_grpc_unary_handler_func)(cwist_http_request *req, cwist_http_response *res, const cwist_grpc_message *message, void *user_ctx)
void(* cwist_grpc_stream_handler_func)(cwist_grpc_stream *stream, void *user_ctx)
int cwist_http2_stream_send_trailers(cwist_h2_stream *stream, const cwist_http2_header *trailers, size_t trailer_count)
int cwist_http2_stream_send_headers(cwist_h2_stream *stream, int status, const cwist_http2_header *headers, size_t header_count, int end_stream)
struct cwist_h2_stream cwist_h2_stream
int cwist_http2_stream_send_data(cwist_h2_stream *stream, const unsigned char *data, size_t len)
Send DATA on a hook-taken stream, honoring flow-control credit. Splits data into peer_max_frame_size ...
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
@ CWIST_HTTP_INTERNAL_ERROR
void cwist_http_request_destroy(cwist_http_request *req)
Destroy an HTTP request object.
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
size_t cwist_http_header_remove(cwist_http_header_node **head, const char *key)
Remove all headers matching a name, honoring per-node ownership (heap vs arena vs borrowed buffers).
void cwist_pb_reader_init(cwist_pb_reader *r, const void *data, size_t len)
Initialize a reader over a caller-owned buffer.
int cwist_pb_write_uint64_field(cwist_pb_writer *w, uint32_t field_number, uint64_t value)
Write a uint64 field as a varint.
void cwist_pb_writer_free(cwist_pb_writer *w)
Release the buffer owned by a Protobuf writer.
void cwist_pb_writer_init(cwist_pb_writer *w)
Initialize a Protobuf writer to an empty buffer.
int cwist_pb_read_field(cwist_pb_reader *r, cwist_pb_field *field)
Read the next field key and payload from the reader.
int cwist_pb_write_bytes_field(cwist_pb_writer *w, uint32_t field_number, const void *data, size_t len)
Write a length-delimited bytes field (key, length varint, raw bytes).
int cwist_pb_write_string_field(cwist_pb_writer *w, uint32_t field_number, const char *value)
Write a string field (length-delimited, excluding the NUL terminator).
Main Application Context.
void * grpc_routes
Unary gRPC route registry.
Integrated error structure used throughout the system.
cwist_http_response * res
const char * status_message
cwist_grpc_status_t status
int32_t retry_pushback_ms
int(* write_frame)(void *ctx, const uint8_t *frame, size_t frame_len, int end_stream)
const cwist_grpc_message * messages
void *(* on_conn_open)(void *user_ctx)
cwist_sstring * path
e.g., "/users/1"
struct cwist_app * app
Owning app context (if any).
cwist_http_header_node * headers
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
cwist_http_status_t status_code
cwist_http_header_node * headers
cwist_pb_wire_type_t wire_type
Backing state for cwist_scratch_alloc()/CWIST_SCRATCH_DEFER: a single block-scoped allocation that pr...
char * data
Access directly only when raw handling is necessary.
Core Application Structure and Lifecycle Management.
void cwist_app_post(cwist_app *app, const char *path, cwist_handler_func handler)
Register a POST handler with default endpoint options.
void cwist_error_dispose(cwist_error_t *err)
Release resources owned by an error value. Currently only CWIST_ERR_JSON owns memory (the cJSON paylo...