[BUG]: Agent Flow API Call block silently sends 'No body' when a JSON-embedded ${variable} contains unescaped quotes (regression of #4187)

Author: SteeleKhoCreated Sep 16, 2026Updated Sep 17, 2026

How are you running AnythingLLM?

Docker (self-hosted), image mintplexlabs/anythingllm, pulled 2026-09-16.

Version

1.16.1 (confirmed via /app/server/package.json inside the running container)

What happened?

An Agent Flow's API Call block, with Request Body type set to JSON and containing a ${variable} reference, intermittently sends a completely empty body (Sending body to <url>: No body), which the target server (in my case, a local Ollama instance) correctly rejects with 400. The flow then reports Flow failed: Unknown error.

This looks related to #4187, which was closed as completed with a comment that it was "solved by switching to version 1.10." However, I've confirmed via the actual source inside a fresh 1.16.1 image that the underlying flaw is still present for a specific, common case: when the interpolated variable's value is free-form LLM-generated text containing an unescaped " or \.

Root cause (confirmed by reading current source in the running container)

server/utils/agentFlows/executor.js, replaceVariables():

javascript
replaceVariables(config) {
  const deepReplace = (obj) => {
    if (typeof obj === "string") {
      return obj.replace(/\${([^}]+)}/g, (match, varName) => {
        const value = this.getValueFromPath(this.variables, varName);
        return value !== undefined ? value : match;
      });
    }
    ...

This splices the variable's raw value directly into the template string with no JSON-escaping. If the value contains a literal ", \, or raw control character (extremely likely for any variable populated by an upstream LLM Instruction block producing multi-paragraph prose/markdown/code), the resulting string is no longer valid JSON.

That corrupted string then reaches server/utils/agentFlows/executors/api-call.js:

javascript
} else if (bodyType === "json") {
  const parsedBody = safeJsonParse(body, null);
  if (parsedBody !== null) {
    requestConfig.body = JSON.stringify(parsedBody);
  }
  requestConfig.headers["Content-Type"] = "application/json";
}
...
introspect(`Sending body to ${url}: ${requestConfig?.body || "No body"}`);
const response = await fetch(url, requestConfig);

When safeJsonParse fails on the corrupted string, it returns the fallback null, the if block is skipped, and requestConfig.body is simply never set. The request is then sent with no body at all, silently, with no error surfaced until the downstream server predictably rejects it.

Why this looks "intermittent"

Whether this triggers depends entirely on whether the upstream LLM's generated text (feeding the variable) happens to contain a quote or backslash — which varies run to run based on the model's phrasing (headers with bold text is fine, but any quoted term, apostrophe-adjacent contraction handled oddly, or a code block with string literals will break it). The same flow, same config, same models can succeed or fail purely based on the LLM's word choice on a given run.

Reproduction steps

  1. Create an Agent Flow with a Flow Variables block defining plan (Static).
  2. Add an LLM Instruction block that writes a multi-paragraph implementation plan into plan (a real-world prompt like "write a step-by-step plan," un-constrained, reliably produces markdown with bold text, code fences, and occasional quotes).
  3. Add an API Call block, POST, Request Body type JSON, body e.g.:
    json
    { "model": "some-model", "prompt": "Implement this plan:\n${plan}", "stream": false }
  4. Run the flow multiple times via @agent. Some runs succeed (body sent correctly); others fail with Sending body to <url>: No body400Flow failed: Unknown error, with no indication of why the body was dropped.

Expected behavior

Either:

  • The variable's value should be JSON-string-escaped before being spliced into a JSON string field (e.g., interpolate against the parsed template — parse the static template to an object first while ${var} placeholders are still inert plain text, then walk the object tree and only escape+splice when a placeholder is embedded inside a larger string; substitute the raw value when a string field is exactly "${var}" so object/array-valued variables still work structurally), or at minimum
  • safeJsonParse failing should throw/report a clear, visible error ("templated body is not valid JSON after variable substitution") instead of silently sending no body at all.

Suggested fix

Escape at interpolation time, scoped to bodyType === "json": when a ${var} match is not the entire string value, JSON-escape the substituted value (JSON.stringify(value).slice(1, -1) equivalent) before splicing. When it is the entire string value, substitute the raw (possibly non-string) value directly so structural JSON variables keep working. This fixes the free-text case here without breaking the "variable holds a whole JSON blob" use case discussed in #4187.

Separately: api-call.js's silent if (parsedBody !== null) swallow should at least surface a real error/log line when parsing fails, rather than silently omitting the body — that alone would have saved a lot of debugging time here.

Source: Mintplex-Labs/anything-llm