]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] Add HTTP content negotiation framework
authorVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 11 Jan 2026 18:08:02 +0000 (18:08 +0000)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Sun, 11 Jan 2026 18:08:02 +0000 (18:08 +0000)
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

src/controller.c
src/libserver/CMakeLists.txt
src/libserver/http/http_router.c
src/libserver/http/http_router.h
src/libserver/http_content_negotiation.c [new file with mode: 0644]
src/libserver/http_content_negotiation.h [new file with mode: 0644]
src/libserver/worker_util.c
src/libserver/worker_util.h

index d3672339c1d0fd01f3ca23eb5bbae1e7486df6f0..a1e8db9e64d1559f3682438db74dc458b2d9794e 100644 (file)
@@ -2740,6 +2740,7 @@ rspamd_controller_handle_savemap(struct rspamd_http_connection_entry *conn_ent,
 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;
@@ -2795,6 +2796,9 @@ rspamd_controller_stat_cleanup_task(void *ud)
 
        rspamd_task_free(cbdata->task);
        ucl_object_unref(cbdata->top);
+       if (cbdata->req_msg) {
+               rspamd_http_message_unref(cbdata->req_msg);
+       }
 }
 
 /*
@@ -3099,7 +3103,7 @@ rspamd_controller_metrics_fin_task(void *ud)
        }
 
        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;
 }
@@ -3134,6 +3138,7 @@ rspamd_controller_handle_metrics_common(
        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,
index 4c0c57869c656781965dd92ada3a73aa05f270f4..ad9df344d1942959336161a7702ff1a2beb9f53b 100644 (file)
@@ -28,6 +28,7 @@ SET(LIBRSPAMDSERVERSRC
         ${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
index 459401e9e430e4fec2c530443bca25f2e5b6a9fb..b46321085ba4cfd12003e8686d77890e181fd4b7 100644 (file)
@@ -335,9 +335,20 @@ rspamd_http_router_finish_handler(struct rspamd_http_connection *conn,
 
                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) {
index 0245fd40e92f6a3a5f3a61f725fb26b35a3a3778..6977d6e736a2125a72c69357a658973ed9d39c86 100644 (file)
@@ -42,6 +42,7 @@ struct rspamd_http_connection_entry {
        gpointer ud;
        gboolean is_reply;
        gboolean support_gzip;
+       unsigned int compression_flags; /* Bitmask of rspamd_http_compression */
        struct rspamd_http_connection_entry *prev, *next;
 };
 
diff --git a/src/libserver/http_content_negotiation.c b/src/libserver/http_content_negotiation.c
new file mode 100644 (file)
index 0000000..68deb8c
--- /dev/null
@@ -0,0 +1,375 @@
+/*
+ * 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);
+}
diff --git a/src/libserver/http_content_negotiation.h b/src/libserver/http_content_negotiation.h
new file mode 100644 (file)
index 0000000..9f65d1a
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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 */
index 298ba4af45d83081abdbf956e5c8cbe08c997f28..4b21d754002cc9d300bec6628ee7821407503f41 100644 (file)
@@ -28,6 +28,7 @@
 #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 */
@@ -727,6 +728,90 @@ void rspamd_controller_send_ucl(struct rspamd_http_connection_entry *entry,
        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)
 {
index 3d096cbbadcfe720e2e25fedd9388b6bca07e219..09d8018dbeeecbc9bed1203ca61eb0402056418e 100644 (file)
@@ -19,6 +19,7 @@
 #include "config.h"
 #include "util.h"
 #include "libserver/http/http_connection.h"
+#include "libserver/http_content_negotiation.h"
 #include "rspamd.h"
 
 #ifdef __cplusplus
@@ -144,6 +145,28 @@ void rspamd_controller_send_string(struct rspamd_http_connection_entry *entry,
 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