Skip to content

out_nats: handshake once per connection, write-completeness checks, respect server max_payload#12159

Open
ChpaMing wants to merge 3 commits into
fluent:masterfrom
ChpaMing:out_nats-upstream-fixes
Open

out_nats: handshake once per connection, write-completeness checks, respect server max_payload#12159
ChpaMing wants to merge 3 commits into
fluent:masterfrom
ChpaMing:out_nats-upstream-fixes

Conversation

@ChpaMing

@ChpaMing ChpaMing commented Jul 26, 2026

Copy link
Copy Markdown

Three independent fixes to the NATS output plugin, one commit each:

1. out_nats: send CONNECT once per connection, not on every flush

cb_nats_flush currently writes NATS_CONNECT before every PUB. With keepalive enabled (the default), every flush on a reused connection repeats the client handshake. The NATS protocol expects CONNECT once per connection; nats-server tolerates the repeats, but it is unnecessary protocol traffic and breaks any consumer/proxy that assumes one CONNECT per connection. Handshake state is now tracked per upstream connection. The flag cannot go stale: connections that errored are never recycled (flb_upstream.c keepalive release path), and connections dropped by the server are destroyed via cb_upstream_conn_ka_dropped.

2. out_nats: treat incomplete writes as flush failure

flb_io_net_write was only checked for ret == -1. A short write (bytes_sent < len) leaves the server with a truncated frame, desynchronizing the protocol stream; the server then discards/closes the connection and the chunk is lost without any error surfaced. Incomplete CONNECT or PUB writes now fail the flush (FLB_RETRY).

3. out_nats: split chunks exceeding server max_payload

The whole chunk is currently published as a single PUB message. nats-server's default max_payload is 1 MB; a larger publish is rejected (-ERR 'Maximum Payload Violation') and the server closes the connection, so the flush can never succeed. We hit this in production collecting Windows event logs (winevtlog replaying channel history), with the server logging e.g. maximum payload exceeded: 1178357 vs 1048576. The flush now batches records into multiple PUB messages, each kept under a fixed 1 MB — deliberately conservative; parsing the server INFO max_payload is noted as a possible follow-up in a code comment. Message shape is unchanged: each message is still a JSON array of [timestamp, record] pairs. A single record larger than the limit is dropped with an error log instead of causing an infinite retry loop.

Out of scope (deliberately not included): NATS authentication (user/pass/token) and platform-specific connection handling. We maintain a downstream fork with those and can propose them separately if there is interest.

N/A


Testing

Example configuration used for the test below:

[SERVICE]
    Flush         1
    Log_Level     debug
    Daemon        off

[INPUT]
    Name   dummy
    Tag    logs.raw.sysmon
    Rate   100
    Dummy  {"message":"dummy","EventID":999}

[OUTPUT]
    Name   nats
    Match  *
    Host   127.0.0.1
    Port   4222

Debug log output (12 s run, ~100 records/s, keepalive connection reused across flushes):

[2026/07/26 12:18:41.195] [debug] [output:nats:nats.0] sent NATS CONNECT handshake
  • CONNECT appears exactly once for the whole run (previously: once per flush), zero [error] lines, zero retries.

  • All records were persisted by the server's JetStream stream (raw_logs, filter logs.raw.>); stream message count grew monotonically during the run, and reading messages back confirmed the unchanged [[ts, {"tag": "logs.raw.sysmon", ...}], ...] shape.

  • The max_payload batching logic is the same code we run in a downstream fork feeding Windows Sysmon/Security events into JetStream in production.

  • Build: MSVC on Windows, plugins/out_nats/nats.c compiles with zero warnings.

  • Example configuration file for the change

  • Debug log output from testing the change

  • Attached Valgrind output that shows no leaks or memory corruption was found

Valgrind: N/A — development and testing for this PR were done on Windows (MSVC), where Valgrind is not available. The changes add one heap allocation per PUB frame (freed on all paths) and reuse the existing sds/json conversion helpers.

Documentation

  • Documentation required for this feature

N/A — no configuration changes; all three fixes preserve existing behavior and message format.

Backporting

  • Backport to latest stable release.

These are bug fixes and would be reasonable backport candidates, but we leave that call to the maintainers.


Fluent Bit is licensed under Apache 2.0, by submitting this pull request I understand that this code will be released under the terms of that license.

Summary by CodeRabbit

  • New Features

    • Added support for publishing large event batches in smaller payloads.
    • Enforced a 1 MB maximum payload size for NATS messages.
    • Added safer handling for oversized individual records.
  • Bug Fixes

    • Improved reliability when sending messages over reused connections.
    • Added safeguards for incomplete writes and broken connections.
    • Prevented oversized records from causing repeated delivery failures.

cham added 3 commits July 26, 2026 12:04
The flush callback wrote the NATS CONNECT handshake on every flush,
even when the upstream layer handed back a recycled keepalive
connection. The server only needs CONNECT right after the TCP
connection is established, so resending it on every flush just adds
noise to the protocol stream.

Track the handshake state in the connection's own user_data field
(unused for upstream connections): connections are zero-initialized
on creation, so a fresh connection always runs the handshake while a
recycled one keeps the mark and skips it. Connections that fail I/O
are destroyed by the upstream layer instead of being recycled, and
the keepalive queue destroys connections dropped by the server, so a
stale mark can never skip the handshake on a new connection.

Signed-off-by: ChpaMing <cham@users.noreply.github.com>
The CONNECT and PUB writes only checked whether flb_io_net_write()
returned -1. A short write (bytes_sent < requested length) was
treated as success, leaving the protocol stream misaligned: the
server then parses the next frame in the middle of the truncated
one and the connection degrades into protocol errors.

Fail the flush with FLB_RETRY whenever fewer bytes than requested
were written, for both the CONNECT handshake and the PUB frame.

Signed-off-by: ChpaMing <cham@users.noreply.github.com>
The plugin used to publish a whole chunk as a single PUB message.
Under load a chunk can grow past the nats-server max_payload limit
(1MB by default), in which case the server rejects the message with
-ERR 'Maximum Payload Violation' and closes the connection: the
flush fails deterministically no matter how many times it is
retried.

Convert and queue records one by one and emit a PUB whenever the
pending payload would cross a fixed 1MB limit. Every message keeps
the usual '[[ts, record], ...]' JSON array shape, so the message
semantics are unchanged for downstream consumers. A fixed limit is
used on purpose instead of parsing max_payload from the server INFO
line, to keep the plugin simple; it matches the server default.

A single record whose own payload exceeds the limit would be
rejected no matter how it is batched and would retry-loop the chunk
forever: log an error, drop it, and continue with the remaining
records.

Signed-off-by: ChpaMing <cham@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The NATS output plugin now converts records individually, batches JSON payloads below a 1 MB limit, sends framed PUB requests with write validation, reuses CONNECT handshakes per connection, drops oversized records, and retries failed flushes.

Changes

NATS payload flow

Layer / File(s) Summary
Record formatting and PUB framing
plugins/out_nats/nats.c
Adds the 1 MB payload limit, converts individual records to JSON array elements, and constructs NATS PUB frames with buffer and incomplete-write checks.
Flush batching and connection lifecycle
plugins/out_nats/nats.c
Reworks flushing to reuse CONNECT handshakes, split records into size-limited batches, drop oversized records, and return retries after publish failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant cb_nats_flush
  participant record_to_json
  participant nats_pub
  participant NATSConnection
  cb_nats_flush->>NATSConnection: reuse connection and send CONNECT once
  cb_nats_flush->>record_to_json: convert decoded event
  record_to_json-->>cb_nats_flush: return JSON element
  cb_nats_flush->>nats_pub: publish size-limited batch
  nats_pub-->>cb_nats_flush: return write status
  cb_nats_flush-->>cb_nats_flush: return FLB_RETRY on failure
Loading

Suggested reviewers: cosmo0920

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main NATS plugin fixes: one handshake per connection, write-completeness checks, and payload sizing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
plugins/out_nats/nats.c (1)

353-363: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider counting dropped records in the plugin metrics.

Records discarded at Line 309 are only logged; there is no drop accounting, so operators see a silent gap between ingested and delivered records. Emitting a drop metric (or at least rate-limiting the error log) would keep drop state observable for this route.

As per coding guidelines: "Preserve route-specific success, retry, and drop state".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/out_nats/nats.c` around lines 353 - 363, Update the record-discard
path around the existing Line 309 handling in the NATS output route to increment
the plugin’s dropped-record metric whenever a record is discarded, while
preserving the existing success and retry accounting. Reuse the established
output-plugin metric or drop-accounting symbol rather than adding a separate
mechanism; keep the current error logging behavior unless required by the metric
implementation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/out_nats/nats.c`:
- Around line 309-319: Update the oversized-record check in the batch-building
flow around elem_len and batch_count to account for the two array wrapper bytes
added to every published payload. Reject records whose serialized size plus '['
and ']' exceeds NATS_MAX_PAYLOAD, ensuring they are dropped before the
empty-batch path can publish an oversized message; keep the existing
batch-splitting behavior for records that fit individually.
- Around line 288-355: Guard every flb_sds_cat call in the batching flow around
json_msg, including initialisation, delimiters, record appends, and final
closing, so allocation failure is detected before json_msg is overwritten.
Preserve and destroy the original buffer on failure, clean up elem, the decoder,
and upstream connection as appropriate, then return FLB_RETRY; a small append
helper may centralize this handling.
- Around line 241-254: Add an error log in the NATS CONNECT write-failure branch
inside the u_conn->user_data initialization block, before releasing the
connection and returning FLB_RETRY. Use flb_plg_error with the plugin context
ctx->ins and include the failed or incomplete CONNECT write details.

---

Nitpick comments:
In `@plugins/out_nats/nats.c`:
- Around line 353-363: Update the record-discard path around the existing Line
309 handling in the NATS output route to increment the plugin’s dropped-record
metric whenever a record is discarded, while preserving the existing success and
retry accounting. Reuse the established output-plugin metric or drop-accounting
symbol rather than adding a separate mechanism; keep the current error logging
behavior unless required by the metric implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93d49b3d-8dd4-42fc-b299-e78adfde965f

📥 Commits

Reviewing files that changed from the base of the PR and between cb7256c and 39c3b14.

📒 Files selected for processing (1)
  • plugins/out_nats/nats.c

Comment thread plugins/out_nats/nats.c
Comment on lines +241 to 254
if (u_conn->user_data == NULL) {
ret = flb_io_net_write(u_conn,
NATS_CONNECT,
sizeof(NATS_CONNECT) - 1,
&bytes_sent);
if (ret == -1 || bytes_sent != sizeof(NATS_CONNECT) - 1) {
/* an incomplete CONNECT leaves the protocol stream misaligned */
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_RETRY);
}

flb_plg_debug(ctx->ins, "sent NATS CONNECT handshake");
u_conn->user_data = u_conn;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect flb_connection definition and all user_data usages on connections
fd -t f 'flb_connection.h' | xargs -r cat -n
rg -nP '\buser_data\b' --type=c --type=h -g '!plugins/out_nats/**' -C2 | rg -n 'conn|connection' -C2

Repository: fluent/fluent-bit

Length of output: 8361


🏁 Script executed:

#!/bin/bash
set -u

echo "== user_data usages (excluding plugins/out_nats/nats.c) =="
rg -n '\buser_data\b' --type=c --type=h plugins include src tests | grep -v 'plugins/out_nats/nats.c' || true

echo
echo "== NATS plugin snippet around handshakes =="
sed -n '220,265p' plugins/out_nats/nats.c | cat -n

echo
echo "== upstream conn setup/release snippets =="
rg -n 'flb_connection_setup|flb_connection_create|flb_upstream_conn_release|av_queue|busy_queue|user_data|cleanup_connection|destroy_conn|keepalive' src include plugins plugins/out include/fluent-bit/server -g '*.{c,h}' | head -n 240

Repository: fluent/fluent-bit

Length of output: 36345


🏁 Script executed:

#!/bin/bash
set -u

echo "== src/flb_connection.c =="
cat -n src/flb_connection.c

echo
echo "== src/flb_upstream.c focused sections =="
rg -n 'flb_upstream_conn_(get|release|cleanup|destroy)|_available|av_queue|user_data|flb_connection_setup|flb_connection_create|prepare_destroy_conn|cleanup_connection|keepalive' src/flb_upstream.c src/flb_upstream.h src/flb_io.c include -g '*.{c,h}' -C3 | sed -n '1,260p'

echo
echo "== exact user_data writes in core C source excluding generated vmlinux headers =="
rg -n '->user_data\s*=|\.user_data\s*=' src plugins/include include/fluent-bit/*.h include/fluent-bit/*/*.h --type=c --type=h | grep -v 'plugins/in_ebpf/traces/includes' || true

Repository: fluent/fluent-bit

Length of output: 24704


🏁 Script executed:

#!/bin/bash
set -u

echo "== exact user_data assignment/write references in C/H excluding eBPF generated headers and nats.c =="
rg -n -e '\->user_data\s*=\s*' -e '\.user_data\s*=\s*' src plugins include/fluent-bit --type=c --type=h | grep -v 'plugins/in_ebpf/traces/includes' || true

echo
echo "== flb_upstream_conn_get/release body =="
sed -n '711,810p' src/flb_upstream.c | cat -n
sed -n '862,925p' src/flb_upstream.c | cat -n

Repository: fluent/fluent-bit

Length of output: 9463


Log the incomplete NATS CONNECT write.

Lines 228-253 document that only new connections send CONNECT, but the write failure path at line 227 has no flb_plg_error, so this may retry silently.

🛠️ Add the missing error log
         if (ret == -1 || bytes_sent != sizeof(NATS_CONNECT) - 1) {
             /* an incomplete CONNECT leaves the protocol stream misaligned */
+            flb_plg_error(ctx->ins, "CONNECT write incomplete (%zu/%zu bytes)",
+                          ret == -1 ? (size_t) 0 : bytes_sent,
+                          sizeof(NATS_CONNECT) - 1);
             flb_upstream_conn_release(u_conn);
             FLB_OUTPUT_RETURN(FLB_RETRY);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (u_conn->user_data == NULL) {
ret = flb_io_net_write(u_conn,
NATS_CONNECT,
sizeof(NATS_CONNECT) - 1,
&bytes_sent);
if (ret == -1 || bytes_sent != sizeof(NATS_CONNECT) - 1) {
/* an incomplete CONNECT leaves the protocol stream misaligned */
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_RETRY);
}
flb_plg_debug(ctx->ins, "sent NATS CONNECT handshake");
u_conn->user_data = u_conn;
}
if (u_conn->user_data == NULL) {
ret = flb_io_net_write(u_conn,
NATS_CONNECT,
sizeof(NATS_CONNECT) - 1,
&bytes_sent);
if (ret == -1 || bytes_sent != sizeof(NATS_CONNECT) - 1) {
/* an incomplete CONNECT leaves the protocol stream misaligned */
flb_plg_error(ctx->ins, "CONNECT write incomplete (%zu/%zu bytes)",
ret == -1 ? (size_t) 0 : bytes_sent,
sizeof(NATS_CONNECT) - 1);
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_RETRY);
}
flb_plg_debug(ctx->ins, "sent NATS CONNECT handshake");
u_conn->user_data = u_conn;
}
🧰 Tools
🪛 Cppcheck (2.21.0)

[error] 243-243: There is an unknown macro here somewhere. Configuration is required. If FLB_VERSION_STR is a macro then please configure it.

(unknownMacro)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/out_nats/nats.c` around lines 241 - 254, Add an error log in the NATS
CONNECT write-failure branch inside the u_conn->user_data initialization block,
before releasing the connection and returning FLB_RETRY. Use flb_plg_error with
the plugin context ctx->ins and include the failed or incomplete CONNECT write
details.

Comment thread plugins/out_nats/nats.c
Comment on lines +288 to +355
json_msg = flb_sds_cat(json_msg, "[", 1);

req_len = snprintf(request, flb_sds_len(event_chunk->tag)+ 32,
"PUB %s %zu\r\n",
event_chunk->tag, json_len);
while ((ret = flb_log_event_decoder_next(
&log_decoder,
&log_event)) == FLB_EVENT_DECODER_SUCCESS) {
elem = record_to_json(ctx, &log_event,
event_chunk->tag, tag_len, config);
if (!elem) {
flb_plg_error(ctx->ins, "failed to convert record to JSON");
flb_sds_destroy(json_msg);
flb_log_event_decoder_destroy(&log_decoder);
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_ERROR);
}
elem_len = flb_sds_len(elem);

/*
* A single record larger than the limit would be rejected no
* matter how it is batched and would retry-loop the chunk
* forever: drop it and continue with the next records.
*/
if (elem_len > NATS_MAX_PAYLOAD) {
flb_plg_error(ctx->ins,
"dropping record of %zu bytes: exceeds NATS "
"max_payload (%d)", elem_len, NATS_MAX_PAYLOAD);
flb_sds_destroy(elem);
continue;
}

/* Append JSON message and ending CRLF */
memcpy(request + req_len, json_msg, json_len);
req_len += json_len;
request[req_len++] = '\r';
request[req_len++] = '\n';
flb_sds_destroy(json_msg);
/* send the current batch if this record would push it over the limit */
if (batch_count > 0 &&
flb_sds_len(json_msg) + 1 + elem_len + 1 > NATS_MAX_PAYLOAD) {
json_msg = flb_sds_cat(json_msg, "]", 1);
if (nats_pub(ctx, u_conn, event_chunk->tag, tag_len,
json_msg) == -1) {
flb_sds_destroy(elem);
flb_sds_destroy(json_msg);
flb_log_event_decoder_destroy(&log_decoder);
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_RETRY);
}
flb_sds_destroy(json_msg);

json_msg = flb_sds_create_size(4096);
if (!json_msg) {
flb_errno();
flb_sds_destroy(elem);
flb_log_event_decoder_destroy(&log_decoder);
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_RETRY);
}
json_msg = flb_sds_cat(json_msg, "[", 1);
batch_count = 0;
}

ret = flb_io_net_write(u_conn, request, req_len, &bytes_sent);
if (ret == -1) {
flb_errno();
flb_free(request);
flb_upstream_conn_release(u_conn);
FLB_OUTPUT_RETURN(FLB_RETRY);
if (batch_count > 0) {
json_msg = flb_sds_cat(json_msg, ",", 1);
}
json_msg = flb_sds_cat(json_msg, elem, elem_len);
flb_sds_destroy(elem);
batch_count++;
}

flb_free(request);
flb_log_event_decoder_destroy(&log_decoder);

/* send the final batch */
if (batch_count > 0) {
json_msg = flb_sds_cat(json_msg, "]", 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unchecked flb_sds_cat results: NULL-deref and leak on allocation failure.

flb_sds_cat returns NULL and leaves the original buffer allocated when the growth realloc fails. Every call site here (Lines 288, 320, 339, 344, 346, 355) assigns the result straight back to json_msg, so an OOM leaks the old buffer and the next flb_sds_len(json_msg) / nats_pub() dereferences NULL. This is reachable precisely on the large-chunk path this PR targets, since batches grow toward 1 MB.

Suggest a small helper that appends and flags failure, then bail out with FLB_RETRY on error.

🛡️ Sketch of a guarded append
+static int nats_sds_cat(flb_sds_t *buf, const char *str, size_t len)
+{
+    flb_sds_t tmp;
+
+    tmp = flb_sds_cat(*buf, str, len);
+    if (!tmp) {
+        return -1;
+    }
+    *buf = tmp;
+    return 0;
+}
-        json_msg = flb_sds_cat(json_msg, elem, elem_len);
-        flb_sds_destroy(elem);
+        ret = nats_sds_cat(&json_msg, elem, elem_len);
+        flb_sds_destroy(elem);
+        if (ret == -1) {
+            flb_errno();
+            flb_sds_destroy(json_msg);
+            flb_log_event_decoder_destroy(&log_decoder);
+            flb_upstream_conn_release(u_conn);
+            FLB_OUTPUT_RETURN(FLB_RETRY);
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/out_nats/nats.c` around lines 288 - 355, Guard every flb_sds_cat call
in the batching flow around json_msg, including initialisation, delimiters,
record appends, and final closing, so allocation failure is detected before
json_msg is overwritten. Preserve and destroy the original buffer on failure,
clean up elem, the decoder, and upstream connection as appropriate, then return
FLB_RETRY; a small append helper may centralize this handling.

Comment thread plugins/out_nats/nats.c
Comment on lines +309 to +319
if (elem_len > NATS_MAX_PAYLOAD) {
flb_plg_error(ctx->ins,
"dropping record of %zu bytes: exceeds NATS "
"max_payload (%d)", elem_len, NATS_MAX_PAYLOAD);
flb_sds_destroy(elem);
continue;
}

/* Append JSON message and ending CRLF */
memcpy(request + req_len, json_msg, json_len);
req_len += json_len;
request[req_len++] = '\r';
request[req_len++] = '\n';
flb_sds_destroy(json_msg);
/* send the current batch if this record would push it over the limit */
if (batch_count > 0 &&
flb_sds_len(json_msg) + 1 + elem_len + 1 > NATS_MAX_PAYLOAD) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Off-by-two: a record between NATS_MAX_PAYLOAD - 1 and NATS_MAX_PAYLOAD still produces an oversized payload.

The drop threshold ignores the [/] wrapper that is always added. A record of exactly NATS_MAX_PAYLOAD bytes passes Line 309, then lands in an empty batch (batch_count == 0, so the split check at Line 318 is skipped) and is published as 1 + elem_len + 1 bytes — a Maximum Payload Violation and the infinite retry loop this branch is meant to prevent.

🐛 Account for the array delimiters in the drop check
-        if (elem_len > NATS_MAX_PAYLOAD) {
+        /* the element is always wrapped in '[' ... ']' */
+        if (elem_len + 2 > NATS_MAX_PAYLOAD) {
             flb_plg_error(ctx->ins,
                           "dropping record of %zu bytes: exceeds NATS "
                           "max_payload (%d)", elem_len, NATS_MAX_PAYLOAD);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (elem_len > NATS_MAX_PAYLOAD) {
flb_plg_error(ctx->ins,
"dropping record of %zu bytes: exceeds NATS "
"max_payload (%d)", elem_len, NATS_MAX_PAYLOAD);
flb_sds_destroy(elem);
continue;
}
/* Append JSON message and ending CRLF */
memcpy(request + req_len, json_msg, json_len);
req_len += json_len;
request[req_len++] = '\r';
request[req_len++] = '\n';
flb_sds_destroy(json_msg);
/* send the current batch if this record would push it over the limit */
if (batch_count > 0 &&
flb_sds_len(json_msg) + 1 + elem_len + 1 > NATS_MAX_PAYLOAD) {
/* the element is always wrapped in '[' ... ']' */
if (elem_len + 2 > NATS_MAX_PAYLOAD) {
flb_plg_error(ctx->ins,
"dropping record of %zu bytes: exceeds NATS "
"max_payload (%d)", elem_len, NATS_MAX_PAYLOAD);
flb_sds_destroy(elem);
continue;
}
/* send the current batch if this record would push it over the limit */
if (batch_count > 0 &&
flb_sds_len(json_msg) + 1 + elem_len + 1 > NATS_MAX_PAYLOAD) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/out_nats/nats.c` around lines 309 - 319, Update the oversized-record
check in the batch-building flow around elem_len and batch_count to account for
the two array wrapper bytes added to every published payload. Reject records
whose serialized size plus '[' and ']' exceeds NATS_MAX_PAYLOAD, ensuring they
are dropped before the empty-batch path can publish an oversized message; keep
the existing batch-splitting behavior for records that fit individually.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39c3b14d87

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread plugins/out_nats/nats.c
* INFO line was considered and deliberately left out to keep the plugin
* simple: the fixed 1MB limit matches the server default.
*/
#define NATS_MAX_PAYLOAD (1024 * 1024)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the advertised max_payload for batching

When a NATS server is configured with max_payload below 1 MiB, this hard-coded value still permits oversized PUB frames. Because nats_pub only verifies that the socket write completed and never reads the server's -ERR, a single oversized final batch can be rejected while the flush returns FLB_OK, permanently releasing and losing the chunk; parse the connection's INFO line and batch against its advertised limit instead.

AGENTS.md reference: AGENTS.md:L267-L271

Useful? React with 👍 / 👎.

Comment thread plugins/out_nats/nats.c
* matter how it is batched and would retry-loop the chunk
* forever: drop it and continue with the next records.
*/
if (elem_len > NATS_MAX_PAYLOAD) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include array delimiters in the single-record limit

For a record whose serialized element is 1,048,575 or 1,048,576 bytes, this check accepts the element even though the actual PUB payload adds the opening and closing array brackets, producing 1,048,577 or 1,048,578 bytes. A default NATS server rejects that frame, while a completed socket write can still make this flush return FLB_OK; the single-record threshold must reserve both delimiter bytes.

AGENTS.md reference: AGENTS.md:L267-L271

Useful? React with 👍 / 👎.

Comment thread plugins/out_nats/nats.c
if (batch_count > 0) {
json_msg = flb_sds_cat(json_msg, ",", 1);
}
json_msg = flb_sds_cat(json_msg, elem, elem_len);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check SDS growth before overwriting the batch pointer

When this append must grow the initial 4 KiB buffer and realloc fails under memory pressure, flb_sds_cat returns NULL while leaving the original allocation intact. Assigning that result directly to json_msg loses the allocation and then lets the next flb_sds_len, flb_sds_cat, or nats_pub dereference NULL, crashing Fluent Bit instead of returning FLB_RETRY; retain the old pointer and check the append result.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants