HTTP Request Node (v2.38.5) Re-templates JSON Response Instead of Parsing Literal Values

Bug Description

The HTTP Request node in n8n version 2.38.5 exhibits unexpected behavior when receiving a JSON response from another n8n workflow’s webhook. Instead of parsing the literal JSON values returned by the webhook, the HTTP Request node re-templates these values back into expression strings (e.g., {{ $json.property }}). This prevents subsequent nodes from accessing the actual data values, as they receive the literal expression strings instead of the resolved data.

Environment Details: n8n is installed locally on my machine and started via the n8n start command in the command prompt (CMD).

To Reproduce

1. Workflow 2 (Webhook Listener):

Create a simple n8n workflow (let’s call it “Workflow 2”).
Start with a Webhook node (HTTP Method: POST, Authentication: None).
Add a Set node to define some data, for example: json
{
“filialAtual”: 300,
“periodo_id”: 1,
“temMais”: true,
“totalAcumulado”: 100
}
Connect the Set node to a Respond to Webhook node.
In the Respond to Webhook node, configure it to return the data from the Set node. For example, using the following JSON structure (which was previously adjusted to avoid syntax errors with null and booleans): json
{
“filialAtual”: “{{ $json.filialAtual }}”,
“periodo_id”: “{{ $json.periodo_id }}”,
“limiteRegistros”: “{{ $json.limiteRegistros }}”,
“lastDtEmissao”: “{{ $json.lastDtEmissao === null ? ‘null’ : $json.lastDtEmissao }}”,
“lastChaveAcesso”: “{{ $json.lastChaveAcesso === null ? ‘null’ : $json.lastChaveAcesso }}”,
“totalAcumulado”: “{{ $json.totalAcumulado }}”,
“temMais”: “{{ $json.temMais === true ? ‘true’ : ‘false’ }}”
}
Publish Workflow 2. Note its production webhook URL (e.g., http://localhost:5678/webhook/your-workflow-2-id).

2.Workflow 1 (HTTP Request Caller):

Create another n8n workflow (let’s call it “Workflow 1”).
Start with a Start node or Manual Trigger.
Add a Set node to define initial data that will be sent to Workflow 2 (e.g., filialAtual: 300, periodo_id: 1, limiteRegistros: 500, etc.).
Add an HTTP Request node (let’s call it “Call Workflow 2”).
Configure the HTTP Request node:
Method: POST
URL: The production webhook URL of Workflow 2.
Authentication: None
Send Body: true
Body Content Type: JSON
Specify Body: Using JSON
JSON Body: Pass the data from the previous Set node using expressions: json
{
“filialAtual”: “{{ $json.filialAtual }}”,
“periodo_id”: “{{ $json.periodo_id }}”,
“limiteRegistros”: “{{ $json.limiteRegistros }}”,
“lastDtEmissao”: “{{ $json.lastDtEmissao }}”,
“lastChaveAcesso”: “{{ $json.lastChaveAcesso }}”,
“totalAcumulado”: “{{ $json.totalAcumulado }}”,
“temMais”: “{{ $json.temMais }}”
}
Execute Workflow 1.

Expected behavior

The HTTP Request node should parse the JSON response from Workflow 2 and present the literal values in its OUTPUT. For example, if Workflow 2 returned {“filialAtual”: 300, “temMais”: true}, the HTTP Request node’s OUTPUT should reflect these literal values:

json
Copiar

[
{
“filialAtual”: 300,
“periodo_id”: 1,
“limiteRegistros”: 500,
“lastDtEmissao”: null,
“lastChaveAcesso”: null,
“totalAcumulado”: 100,
“temMais”: true
}
]

Debug Info

Debug info

core

  • n8nVersion: 2.38.5
  • platform: npm
  • nodeJsVersion: 24.19.0
  • nodeEnv: undefined
  • database: sqlite
  • executionMode: regular
  • concurrency: -1
  • license: community
  • consumerId: unknown

storage

  • success: all
  • error: all
  • progress: false
  • manual: true
  • binaryMode: filesystem

pruning

  • enabled: true
  • maxAge: 336 hours
  • maxCount: 10000 executions

client

  • userAgent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/152.0.0.0 safari/537.36
  • isTouchDevice: false

cluster

  • instanceCount: 1
  • versions: 2.38.5
  • instances:
    • instanceKey: 4fcbd15f-f437-4e49-8624-bdfcb3ff03c6, hostId: main-cdKCNrkWxZd1KQ30, instanceType: main, instanceRole: leader, version: 2.38.5
  • checks:
    • check: hostid-clash, status: succeeded, warnings: -
    • check: lifecycle, status: succeeded, warnings: -
    • check: split-brain, status: succeeded, warnings: -
    • check: version-mismatch, status: succeeded, warnings: -

Generated at: 2026-09-11T16:57:34.026Z

Operating System

Windows 11 Pro

n8n Version

2.38.5

Node.js Version

24.19.0

Database

SQLite (default)

Execution mode

main (default)

Hosting

self hosted

The HTTP Request node is not re-templating anything. It is printing, faithfully, the bytes
Workflow 2 actually sent it — and Workflow 2 really did send the literal text
{{ $json.filialAtual }}. The fault is one field upstream of where you are looking, and you
can prove which in about thirty seconds without changing a single node.

In the Respond to Webhook node, the JSON body field is in Fixed mode, not Expression mode.
n8n only evaluates {{ ... }} inside a field that has been switched to an expression. A fixed
field is treated as a string constant, braces and all. So Workflow 2’s response body is the
literal characters, Workflow 2’s Set node output is never consulted, and by the time Workflow 1
receives it there is nothing left to resolve — which is exactly why no amount of work on the
HTTP Request node has moved it.

The decisive read-only check, so you are not taking my word for it. Open Workflow 2 →
... menu → Download, and open the JSON. Find the Respond to Webhook node and look at
parameters.responseBody:

"responseBody": "={\"filialAtual\": \"{{ $json.filialAtual }}\", ...     <- expression (leading =)
"responseBody": "{\"filialAtual\": \"{{ $json.filialAtual }}\", ...      <- fixed  (no leading =)

n8n stores expression fields with a leading = and fixed fields without one. If that =
is absent, that is your whole bug, it is not a 2.38.5 regression, and nothing downstream needs
to change. Fix it by hovering the field and switching Fixed → Expression (or by clicking the
= toggle), then re-run.

Now the part that will bite you immediately afterwards, because your Expected Behavior asks for
300 and true, and the fix above will hand you "300" and "true":

Quoted interpolation always produces a string. "filialAtual": "{{ $json.filialAtual }}"
substitutes into a JSON string literal, so you get "300", never 300. To emit real JSON
types, drop the quotes and let the value land raw:

{
  "filialAtual": {{ $json.filialAtual }},
  "periodo_id": {{ $json.periodo_id }},
  "limiteRegistros": {{ $json.limiteRegistros }},
  "lastDtEmissao": {{ JSON.stringify($json.lastDtEmissao ?? null) }},
  "lastChaveAcesso": {{ JSON.stringify($json.lastChaveAcesso ?? null) }},
  "totalAcumulado": {{ $json.totalAcumulado }},
  "temMais": {{ $json.temMais }}
}

JSON.stringify on the two nullable fields is deliberate: it emits a correctly quoted string
when there is a value and a bare null when there is not, which is the one case hand-built JSON
gets wrong. Simpler still, if the shape can match: set Respond With: First Incoming Item and
delete the hand-written body entirely — then types are preserved by construction and this class
of bug cannot recur.

The two conversions in your current body are a live defect, not a style point, and given the
field names — temMais, lastChaveAcesso, lastDtEmissao, totalAcumulado — I read this as a
pagination loop over NF-e / invoice pages. Both conversions break it silently:

  1. "{{ $json.temMais === true ? 'true' : 'false' }}" yields the string "false" when
    there are no more pages. In JavaScript a non-empty string is truthy, so an IF node testing
    temMais as a boolean takes the true branch on the stop condition. Your loop does not
    terminate when the data says to stop — it terminates when something else happens to break,
    or it keeps paging. No node errors, no red execution; the run goes green.
  2. "{{ $json.lastDtEmissao === null ? 'null' : ... }}" yields the string "null", which
    is never === null and never empty. Any “is this the first page?” test built on that field
    is permanently false, so page one is treated as a continuation and your cursor starts from a
    value that does not exist.

Both of those produce a workflow that reports success while totalAcumulado is wrong, which is
the failure mode worth catching before it is in production for a month.

Order I would run this in: confirm the missing = from the downloaded JSON first, because if it
is absent everything else in this thread is a consequence of it. Then unquote the numerics and
booleans. Then re-check the loop’s stop condition against a real temMais: false page.

I will say the commercial part outright rather than leave it implied: I do this for money —
production automation reliability, specifically silent failures where the run goes green and the
data is wrong, which is precisely the second half of what is above. If you would rather have it
done than described, the bounded version is US$150 fixed, 48 hours, one revision. You send
read-only artifacts you export yourself — the downloaded JSON for both workflows with
credentials stripped, and one execution’s input/output for the HTTP Request node. No access to
your systems, no credentials, nothing installed. I return the exact field the break is in,
quoting the line in your own export that proves it, plus the change and the check that confirms
it held. Acceptance check, objective: the HTTP Request node’s OUTPUT shows filialAtual as the
number 300 and temMais as the boolean true — not the strings "300" / "true",
and not {{ $json.filialAtual }} — and one full pagination pass stops on temMais: false
instead of continuing. If that does not happen, you pay nothing.

Either way, download Workflow 2 and look for that leading = first. If it is missing you have
the answer for free and none of the rest of this is your problem.