Add content type negotiation based on Accept header for HTTP responses.
This allows clients like DataDog's OpenMetrics scraper to receive
responses with Content-Type matching their Accept header preferences.
- Add http_content_negotiation.c/h with Accept header parsing
- Support quality factors (q=) in Accept header
- Parse Accept-Encoding for gzip/zstd/deflate support
- Add rspamd_controller_send_openmetrics_negotiated()
- Update /metrics endpoint to negotiate Content-Type
- Fallback to text/plain for Prometheus 0.0.4 compatibility
struct rspamd_stat_cbdata {
struct rspamd_http_connection_entry *conn_ent;
struct rspamd_controller_worker_ctx *ctx;
+ struct rspamd_http_message *req_msg;
ucl_object_t *top;
ucl_object_t *stat;
struct rspamd_task *task;
rspamd_task_free(cbdata->task);
ucl_object_unref(cbdata->top);
+ if (cbdata->req_msg) {
+ rspamd_http_message_unref(cbdata->req_msg);
+ }
}
/*
}
rspamd_printf_fstring(&output, "# EOF\n");
- rspamd_controller_send_openmetrics(conn_ent, output);
+ rspamd_controller_send_openmetrics_negotiated(conn_ent, cbdata->req_msg, output);
return TRUE;
}
cbdata->task = task;
cbdata->ctx = ctx;
cbdata->top = top;
+ cbdata->req_msg = rspamd_http_message_ref(msg);
task->s = rspamd_session_create(session->pool,
rspamd_controller_metrics_fin_task,
${CMAKE_CURRENT_SOURCE_DIR}/task.c
${CMAKE_CURRENT_SOURCE_DIR}/url.c
${CMAKE_CURRENT_SOURCE_DIR}/worker_util.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/http_content_negotiation.c
${CMAKE_CURRENT_SOURCE_DIR}/logger/logger.c
${CMAKE_CURRENT_SOURCE_DIR}/logger/logger_file.c
${CMAKE_CURRENT_SOURCE_DIR}/logger/logger_syslog.c
encoding = rspamd_http_message_find_header(msg, "Accept-Encoding");
- if (encoding && rspamd_substring_search(encoding->begin, encoding->len,
- "gzip", 4) != -1) {
- entry->support_gzip = TRUE;
+ if (encoding) {
+ if (rspamd_substring_search(encoding->begin, encoding->len,
+ "gzip", 4) != -1) {
+ entry->support_gzip = TRUE;
+ entry->compression_flags |= (1 << 0); /* RSPAMD_HTTP_COMPRESS_GZIP */
+ }
+ if (rspamd_substring_search(encoding->begin, encoding->len,
+ "zstd", 4) != -1) {
+ entry->compression_flags |= (1 << 1); /* RSPAMD_HTTP_COMPRESS_ZSTD */
+ }
+ if (rspamd_substring_search(encoding->begin, encoding->len,
+ "deflate", 7) != -1) {
+ entry->compression_flags |= (1 << 2); /* RSPAMD_HTTP_COMPRESS_DEFLATE */
+ }
}
if (handler != NULL) {
gpointer ud;
gboolean is_reply;
gboolean support_gzip;
+ unsigned int compression_flags; /* Bitmask of rspamd_http_compression */
struct rspamd_http_connection_entry *prev, *next;
};
--- /dev/null
+/*
+ * Copyright 2026 Vsevolod Stakhov
+ *
+ * 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
+ *
+ * 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 "http_content_negotiation.h"
+#include "http/http_message.h"
+#include "str_util.h"
+#include "ucl.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+struct rspamd_content_type_mapping {
+ const char *mime_type;
+ const char *full_type_str;
+ enum rspamd_http_content_type type_enum;
+ int ucl_emit_type;
+};
+
+static const struct rspamd_content_type_mapping content_type_map[] = {
+ {
+ .mime_type = "application/json",
+ .full_type_str = "application/json",
+ .type_enum = RSPAMD_HTTP_CTYPE_JSON,
+ .ucl_emit_type = UCL_EMIT_JSON_COMPACT,
+ },
+ {
+ .mime_type = "application/msgpack",
+ .full_type_str = "application/msgpack",
+ .type_enum = RSPAMD_HTTP_CTYPE_MSGPACK,
+ .ucl_emit_type = UCL_EMIT_MSGPACK,
+ },
+ {
+ .mime_type = "application/openmetrics-text",
+ .full_type_str = "application/openmetrics-text; version=1.0.0; charset=utf-8",
+ .type_enum = RSPAMD_HTTP_CTYPE_OPENMETRICS,
+ .ucl_emit_type = -1,
+ },
+ {
+ .mime_type = "text/plain",
+ .full_type_str = "text/plain; version=0.0.4; charset=utf-8",
+ .type_enum = RSPAMD_HTTP_CTYPE_TEXT_PLAIN,
+ .ucl_emit_type = -1,
+ },
+ {
+ .mime_type = "application/octet-stream",
+ .full_type_str = "application/octet-stream",
+ .type_enum = RSPAMD_HTTP_CTYPE_OCTET_STREAM,
+ .ucl_emit_type = -1,
+ },
+ {NULL, NULL, RSPAMD_HTTP_CTYPE_UNKNOWN, -1},
+};
+
+const char *
+rspamd_http_content_type_string(enum rspamd_http_content_type ctype)
+{
+ for (gsize i = 0; content_type_map[i].mime_type != NULL; i++) {
+ if (content_type_map[i].type_enum == ctype) {
+ return content_type_map[i].full_type_str;
+ }
+ }
+
+ return "application/octet-stream";
+}
+
+const char *
+rspamd_http_content_type_mime(enum rspamd_http_content_type ctype)
+{
+ for (gsize i = 0; content_type_map[i].mime_type != NULL; i++) {
+ if (content_type_map[i].type_enum == ctype) {
+ return content_type_map[i].mime_type;
+ }
+ }
+
+ return "application/octet-stream";
+}
+
+static int
+rspamd_http_content_type_ucl_emit(enum rspamd_http_content_type ctype)
+{
+ for (gsize i = 0; content_type_map[i].mime_type != NULL; i++) {
+ if (content_type_map[i].type_enum == ctype) {
+ return content_type_map[i].ucl_emit_type;
+ }
+ }
+
+ return -1;
+}
+
+static gboolean
+rspamd_http_content_type_matches(const char *media_type, gsize media_len,
+ enum rspamd_http_content_type desired)
+{
+ const char *desired_mime = rspamd_http_content_type_mime(desired);
+ const char *slash;
+ gsize desired_len;
+
+ if (media_len == 3 && memcmp(media_type, "*/*", 3) == 0) {
+ return TRUE;
+ }
+
+ slash = memchr(media_type, '/', media_len);
+ if (slash && media_len > 2) {
+ gsize type_len = slash - media_type;
+ if (media_len >= type_len + 2 && media_type[type_len + 1] == '*') {
+ const char *desired_slash = strchr(desired_mime, '/');
+ if (desired_slash) {
+ gsize desired_type_len = desired_slash - desired_mime;
+ if (type_len == desired_type_len &&
+ g_ascii_strncasecmp(media_type, desired_mime, type_len) == 0) {
+ return TRUE;
+ }
+ }
+ }
+ }
+
+ desired_len = strlen(desired_mime);
+ if (media_len == desired_len &&
+ g_ascii_strncasecmp(media_type, desired_mime, media_len) == 0) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+rspamd_http_parse_media_range(const char *start, gsize len,
+ const enum rspamd_http_content_type *desired,
+ enum rspamd_http_content_type *out_type,
+ double *out_quality)
+{
+ const char *end = start + len;
+ const char *semicolon;
+ const char *q_pos;
+ gsize mime_len;
+ double quality = 1.0;
+
+ while (start < end && g_ascii_isspace(*start)) {
+ start++;
+ }
+ while (end > start && g_ascii_isspace(*(end - 1))) {
+ end--;
+ }
+
+ if (start >= end) {
+ return FALSE;
+ }
+
+ semicolon = memchr(start, ';', end - start);
+
+ if (semicolon) {
+ mime_len = semicolon - start;
+
+ while (mime_len > 0 && g_ascii_isspace(start[mime_len - 1])) {
+ mime_len--;
+ }
+
+ q_pos = semicolon + 1;
+ while (q_pos < end) {
+ while (q_pos < end && g_ascii_isspace(*q_pos)) {
+ q_pos++;
+ }
+
+ if (q_pos + 2 <= end &&
+ (q_pos[0] == 'q' || q_pos[0] == 'Q') &&
+ q_pos[1] == '=') {
+ char *endptr;
+ double q;
+
+ q_pos += 2;
+ q = strtod(q_pos, &endptr);
+ if (endptr > q_pos) {
+ quality = CLAMP(q, 0.0, 1.0);
+ }
+ break;
+ }
+
+ q_pos = memchr(q_pos, ';', end - q_pos);
+ if (q_pos) {
+ q_pos++;
+ }
+ else {
+ break;
+ }
+ }
+ }
+ else {
+ mime_len = end - start;
+ }
+
+ if (mime_len == 0) {
+ return FALSE;
+ }
+
+ for (const enum rspamd_http_content_type *d = desired;
+ *d != RSPAMD_HTTP_CTYPE_UNKNOWN; d++) {
+ if (rspamd_http_content_type_matches(start, mime_len, *d)) {
+ *out_type = *d;
+ *out_quality = quality;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+enum rspamd_http_content_type
+rspamd_http_parse_accept_header(const rspamd_ftok_t *accept_hdr,
+ const enum rspamd_http_content_type *desired,
+ double *out_quality)
+{
+ if (!accept_hdr || accept_hdr->len == 0 || !desired) {
+ if (out_quality) {
+ *out_quality = 1.0;
+ }
+ return RSPAMD_HTTP_CTYPE_UNKNOWN;
+ }
+
+ double best_quality = -1.0;
+ enum rspamd_http_content_type best_type = RSPAMD_HTTP_CTYPE_UNKNOWN;
+
+ const char *cur = accept_hdr->begin;
+ const char *end = accept_hdr->begin + accept_hdr->len;
+
+ while (cur < end) {
+ const char *comma = memchr(cur, ',', end - cur);
+ gsize range_len = comma ? (gsize) (comma - cur) : (gsize) (end - cur);
+
+ enum rspamd_http_content_type match_type;
+ double match_quality;
+
+ if (rspamd_http_parse_media_range(cur, range_len, desired,
+ &match_type, &match_quality)) {
+ if (match_quality > best_quality) {
+ best_quality = match_quality;
+ best_type = match_type;
+ }
+ }
+
+ cur = comma ? comma + 1 : end;
+ }
+
+ if (out_quality) {
+ *out_quality = best_quality > 0 ? best_quality : 1.0;
+ }
+
+ return best_type;
+}
+
+unsigned int
+rspamd_http_parse_accept_encoding(const rspamd_ftok_t *encoding_hdr)
+{
+ unsigned int flags = RSPAMD_HTTP_COMPRESS_NONE;
+
+ if (!encoding_hdr || encoding_hdr->len == 0) {
+ return flags;
+ }
+
+ if (rspamd_substring_search_caseless(encoding_hdr->begin, encoding_hdr->len,
+ "gzip", 4) != -1) {
+ flags |= RSPAMD_HTTP_COMPRESS_GZIP;
+ }
+
+ if (rspamd_substring_search_caseless(encoding_hdr->begin, encoding_hdr->len,
+ "zstd", 4) != -1) {
+ flags |= RSPAMD_HTTP_COMPRESS_ZSTD;
+ }
+
+ if (rspamd_substring_search_caseless(encoding_hdr->begin, encoding_hdr->len,
+ "deflate", 7) != -1) {
+ flags |= RSPAMD_HTTP_COMPRESS_DEFLATE;
+ }
+
+ return flags;
+}
+
+gboolean
+rspamd_http_negotiate_content(struct rspamd_http_message *msg,
+ const enum rspamd_http_content_type *desired,
+ enum rspamd_http_content_type fallback,
+ struct rspamd_content_negotiation_result *result)
+{
+ const rspamd_ftok_t *accept_hdr;
+ const rspamd_ftok_t *encoding_hdr;
+
+ if (!result || !desired) {
+ return FALSE;
+ }
+
+ memset(result, 0, sizeof(*result));
+ result->content_type = desired[0];
+ result->content_type_str = rspamd_http_content_type_string(desired[0]);
+ result->compression_flags = RSPAMD_HTTP_COMPRESS_NONE;
+ result->preferred_compression = RSPAMD_HTTP_COMPRESS_NONE;
+ result->content_type_quality = 1.0;
+ result->negotiation_success = FALSE;
+ result->ucl_emit_type = rspamd_http_content_type_ucl_emit(desired[0]);
+
+ if (!msg) {
+ result->negotiation_success = TRUE;
+ return TRUE;
+ }
+
+ accept_hdr = rspamd_http_message_find_header(msg, "Accept");
+
+ if (accept_hdr && accept_hdr->len > 0) {
+ double quality;
+ enum rspamd_http_content_type matched =
+ rspamd_http_parse_accept_header(accept_hdr, desired, &quality);
+
+ if (matched != RSPAMD_HTTP_CTYPE_UNKNOWN && quality > 0) {
+ result->content_type = matched;
+ result->content_type_str = rspamd_http_content_type_string(matched);
+ result->content_type_quality = quality;
+ result->negotiation_success = TRUE;
+ result->ucl_emit_type = rspamd_http_content_type_ucl_emit(matched);
+ }
+ else if (fallback != RSPAMD_HTTP_CTYPE_UNKNOWN) {
+ result->content_type = fallback;
+ result->content_type_str = rspamd_http_content_type_string(fallback);
+ result->content_type_quality = 0.0;
+ result->negotiation_success = FALSE;
+ result->ucl_emit_type = rspamd_http_content_type_ucl_emit(fallback);
+ }
+ }
+ else {
+ result->negotiation_success = TRUE;
+ }
+
+ encoding_hdr = rspamd_http_message_find_header(msg, "Accept-Encoding");
+
+ if (encoding_hdr && encoding_hdr->len > 0) {
+ result->compression_flags = rspamd_http_parse_accept_encoding(encoding_hdr);
+
+ if (result->compression_flags & RSPAMD_HTTP_COMPRESS_ZSTD) {
+ result->preferred_compression = RSPAMD_HTTP_COMPRESS_ZSTD;
+ }
+ else if (result->compression_flags & RSPAMD_HTTP_COMPRESS_GZIP) {
+ result->preferred_compression = RSPAMD_HTTP_COMPRESS_GZIP;
+ }
+ else if (result->compression_flags & RSPAMD_HTTP_COMPRESS_DEFLATE) {
+ result->preferred_compression = RSPAMD_HTTP_COMPRESS_DEFLATE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+rspamd_http_negotiate_content_simple(struct rspamd_http_message *msg,
+ enum rspamd_http_content_type desired,
+ enum rspamd_http_content_type fallback,
+ struct rspamd_content_negotiation_result *result)
+{
+ enum rspamd_http_content_type desired_arr[] = {
+ desired,
+ RSPAMD_HTTP_CTYPE_UNKNOWN,
+ };
+
+ return rspamd_http_negotiate_content(msg, desired_arr, fallback, result);
+}
--- /dev/null
+/*
+ * Copyright 2026 Vsevolod Stakhov
+ *
+ * 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
+ *
+ * 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.
+ */
+#ifndef RSPAMD_HTTP_CONTENT_NEGOTIATION_H
+#define RSPAMD_HTTP_CONTENT_NEGOTIATION_H
+
+#include "config.h"
+#include "fstring.h"
+#include "http/http_message.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum rspamd_http_content_type {
+ RSPAMD_HTTP_CTYPE_JSON = 0,
+ RSPAMD_HTTP_CTYPE_MSGPACK,
+ RSPAMD_HTTP_CTYPE_OPENMETRICS,
+ RSPAMD_HTTP_CTYPE_TEXT_PLAIN,
+ RSPAMD_HTTP_CTYPE_OCTET_STREAM,
+ RSPAMD_HTTP_CTYPE_UNKNOWN
+};
+
+enum rspamd_http_compression {
+ RSPAMD_HTTP_COMPRESS_NONE = 0,
+ RSPAMD_HTTP_COMPRESS_GZIP = (1 << 0),
+ RSPAMD_HTTP_COMPRESS_ZSTD = (1 << 1),
+ RSPAMD_HTTP_COMPRESS_DEFLATE = (1 << 2)
+};
+
+struct rspamd_content_negotiation_result {
+ enum rspamd_http_content_type content_type;
+ const char *content_type_str;
+ unsigned int compression_flags;
+ enum rspamd_http_compression preferred_compression;
+ double content_type_quality;
+ gboolean negotiation_success;
+ int ucl_emit_type;
+};
+
+/**
+ * Negotiate content type and encoding from HTTP request
+ *
+ * @param msg HTTP message containing Accept and Accept-Encoding headers
+ * @param desired Array of desired content types in preference order,
+ * terminated with RSPAMD_HTTP_CTYPE_UNKNOWN
+ * @param fallback Fallback content type if negotiation fails (can be UNKNOWN for no fallback)
+ * @param result Output: negotiation result structure
+ * @return TRUE if negotiation produced a valid result, FALSE on error
+ */
+gboolean rspamd_http_negotiate_content(
+ struct rspamd_http_message *msg,
+ const enum rspamd_http_content_type *desired,
+ enum rspamd_http_content_type fallback,
+ struct rspamd_content_negotiation_result *result);
+
+/**
+ * Simplified negotiation for single desired type with fallback
+ *
+ * @param msg HTTP message
+ * @param desired Single desired content type
+ * @param fallback Fallback type if client doesn't accept desired
+ * @param result Output: negotiation result
+ * @return TRUE on success
+ */
+gboolean rspamd_http_negotiate_content_simple(
+ struct rspamd_http_message *msg,
+ enum rspamd_http_content_type desired,
+ enum rspamd_http_content_type fallback,
+ struct rspamd_content_negotiation_result *result);
+
+/**
+ * Parse Accept header and return best matching content type
+ *
+ * @param accept_hdr Accept header value (can be NULL)
+ * @param desired Array of desired types, terminated with UNKNOWN
+ * @param out_quality Output: quality factor of best match (can be NULL)
+ * @return Best matching content type or RSPAMD_HTTP_CTYPE_UNKNOWN
+ */
+enum rspamd_http_content_type rspamd_http_parse_accept_header(
+ const rspamd_ftok_t *accept_hdr,
+ const enum rspamd_http_content_type *desired,
+ double *out_quality);
+
+/**
+ * Parse Accept-Encoding header
+ *
+ * @param encoding_hdr Accept-Encoding header value (can be NULL)
+ * @return Bitfield of supported compression types
+ */
+unsigned int rspamd_http_parse_accept_encoding(
+ const rspamd_ftok_t *encoding_hdr);
+
+/**
+ * Get content type string for a given enum value
+ *
+ * @param ctype Content type enum
+ * @return Static string for Content-Type header (with parameters like charset)
+ */
+const char *rspamd_http_content_type_string(
+ enum rspamd_http_content_type ctype);
+
+/**
+ * Get MIME type string (without parameters) for a given enum value
+ *
+ * @param ctype Content type enum
+ * @return Static MIME type string (e.g., "application/json")
+ */
+const char *rspamd_http_content_type_mime(
+ enum rspamd_http_content_type ctype);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* RSPAMD_HTTP_CONTENT_NEGOTIATION_H */
#include "libserver/maps/map_helpers.h"
#include "libserver/http/http_private.h"
#include "libserver/http/http_router.h"
+#include "libserver/http_content_negotiation.h"
#include "libutil/rrd.h"
/* sys/resource.h */
entry->is_reply = TRUE;
}
+void rspamd_controller_send_openmetrics_negotiated(
+ struct rspamd_http_connection_entry *entry,
+ struct rspamd_http_message *req_msg,
+ rspamd_fstring_t *str)
+{
+ struct rspamd_http_message *msg;
+ struct rspamd_content_negotiation_result neg_result;
+
+ /* Desired content types for metrics: OpenMetrics preferred, text/plain fallback */
+ static const enum rspamd_http_content_type metrics_types[] = {
+ RSPAMD_HTTP_CTYPE_OPENMETRICS,
+ RSPAMD_HTTP_CTYPE_TEXT_PLAIN,
+ RSPAMD_HTTP_CTYPE_UNKNOWN,
+ };
+
+ /* Negotiate content type */
+ rspamd_http_negotiate_content(req_msg, metrics_types,
+ RSPAMD_HTTP_CTYPE_TEXT_PLAIN, &neg_result);
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = 200;
+ msg->status = rspamd_fstring_new_init("OK", 2);
+
+ rspamd_http_message_set_body_from_fstring_steal(msg,
+ rspamd_controller_maybe_compress(entry, str, msg));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_router_insert_headers(entry->rt, msg);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ neg_result.content_type_str,
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+}
+
+void rspamd_controller_send_ucl_negotiated(
+ struct rspamd_http_connection_entry *entry,
+ struct rspamd_http_message *req_msg,
+ ucl_object_t *obj)
+{
+ struct rspamd_http_message *msg;
+ struct rspamd_content_negotiation_result neg_result;
+ rspamd_fstring_t *reply;
+
+ /* Desired content types for UCL: JSON preferred, msgpack if requested */
+ static const enum rspamd_http_content_type ucl_types[] = {
+ RSPAMD_HTTP_CTYPE_JSON,
+ RSPAMD_HTTP_CTYPE_MSGPACK,
+ RSPAMD_HTTP_CTYPE_UNKNOWN,
+ };
+
+ /* Negotiate content type */
+ rspamd_http_negotiate_content(req_msg, ucl_types,
+ RSPAMD_HTTP_CTYPE_JSON, &neg_result);
+
+ msg = rspamd_http_new_message(HTTP_RESPONSE);
+ msg->date = time(NULL);
+ msg->code = 200;
+ msg->status = rspamd_fstring_new_init("OK", 2);
+ reply = rspamd_fstring_sized_new(BUFSIZ);
+
+ /* Use negotiated UCL emit type */
+ if (neg_result.ucl_emit_type >= 0) {
+ rspamd_ucl_emit_fstring(obj, neg_result.ucl_emit_type, &reply);
+ }
+ else {
+ rspamd_ucl_emit_fstring(obj, UCL_EMIT_JSON_COMPACT, &reply);
+ }
+
+ rspamd_http_message_set_body_from_fstring_steal(msg,
+ rspamd_controller_maybe_compress(entry, reply, msg));
+ rspamd_http_connection_reset(entry->conn);
+ rspamd_http_router_insert_headers(entry->rt, msg);
+ rspamd_http_connection_write_message(entry->conn,
+ msg,
+ NULL,
+ neg_result.content_type_str,
+ entry,
+ entry->rt->timeout);
+ entry->is_reply = TRUE;
+}
+
static void
rspamd_worker_drop_priv(struct rspamd_main *rspamd_main)
{
#include "config.h"
#include "util.h"
#include "libserver/http/http_connection.h"
+#include "libserver/http_content_negotiation.h"
#include "rspamd.h"
#ifdef __cplusplus
void rspamd_controller_send_ucl(struct rspamd_http_connection_entry *entry,
ucl_object_t *obj);
+/**
+ * Send openmetrics-formatted strings using HTTP with content negotiation
+ * @param entry router entry
+ * @param msg original HTTP request message (for Accept header parsing)
+ * @param str rspamd fstring buffer, ownership is transferred
+ */
+void rspamd_controller_send_openmetrics_negotiated(
+ struct rspamd_http_connection_entry *entry,
+ struct rspamd_http_message *msg,
+ rspamd_fstring_t *str);
+
+/**
+ * Send UCL using HTTP with content negotiation (supports JSON and msgpack)
+ * @param entry router entry
+ * @param msg original HTTP request message (for Accept header parsing)
+ * @param obj object to send
+ */
+void rspamd_controller_send_ucl_negotiated(
+ struct rspamd_http_connection_entry *entry,
+ struct rspamd_http_message *msg,
+ ucl_object_t *obj);
+
/**
* Return worker's control structure by its type
* @param type