Skip to content

fix(HTTP Request Node): Show the API response when a file upload fails - #36924

Open
BerniWittmann wants to merge 2 commits into
masterfrom
node-5292-http-request-node-empty-output-panel-on-post-file-upload
Open

fix(HTTP Request Node): Show the API response when a file upload fails#36924
BerniWittmann wants to merge 2 commits into
masterfrom
node-5292-http-request-node-empty-output-panel-on-post-file-upload

Conversation

@BerniWittmann

@BerniWittmann BerniWittmann commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

An HTTP Request node that POSTs a file and gets a 4xx back rendered an empty output panel. The toast showed Bad request - please check your parameters, but the API's actual response was nowhere to be seen.

The response body was never lost — the error view crashed while rendering.

  1. A binary upload read from external binary storage (filesystem/s3, the cloud default) stays a live Readable, not a Buffer.
  2. sanitizeUiMessage only swapped the body out when it was a Buffer over 250 KB, so the stream passed through into error.context.request.
  3. While the request is in flight, that stream's _readableState.pipes chain is circular (res → socket → _httpMessage).
  4. Execution data travels as flatted, which faithfully restores the cycle in the browser.
  5. NodeErrorView interpolated the value. Vue's toDisplayString calls JSON.stringify, which threw, and the render error tore down the whole error view.

This is why it only appeared with real files: a small inline binary is a serializable Buffer, a stored one is a stream.

The fix has two halves. sanitizeUiMessage now replaces upload streams before deepCopy preserves the cycle — covering body, a form-data instance (node version 4.2+), and the plain { field: { value, options } } map used below 4.2 and in V1/V2. NodeErrorView renders sanitized copies, so an execution already recorded before this fix still shows its error after an upgrade.

Before After
OUTPUT ⚠ + 1 item, blank body Error box with the API's message

Before:

OUTPUT ⚠ ⓘ
1 item
                          ← nothing
Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'res' -> object with constructor 'Object'
    |     property 'socket' -> object with constructor 'Object'
    --- property '_httpMessage' closes the circle
    at JSON.stringify (<anonymous>)
    at toDisplayString (vue.runtime.esm-bundler…)

After:

OUTPUT ⚠ ⓘ
1 item
  Bad request - please check your parameters
  file exceeds the maximum allowed size          ← the API's response

  Error details ▸ From HTTP Request
    Error code    400
    Full message  400 - "{"type":"error","error":{"type":"invalid_request_error",
                  "message":"file exceeds the maximum allowed size"}}"
    Request       { "body": "Binary data got replaced with this text. Original
                  was a stream.", "headers": { … }, "method": "POST", … }

How to test

The bug needs binary data stored outside the run data — with the default binary mode the upload is an inline Buffer, which was always serializable, and nothing reproduces.

1. Save the mock API below as mock-server.mjs and start it:

node mock-server.mjs
mock-server.mjs
// Mock file-upload API: serves a ~6 MB PDF, and rejects any upload with a 400 + JSON body.
import { createServer } from 'node:http';

const PORT = Number(process.env.PORT ?? 4599);

const server = createServer((req, res) => {
	const url = new URL(req.url, `http://${req.headers.host}`);
	console.log(`[mock] ${req.method} ${url.pathname} content-length=${req.headers['content-length']}`);

	if (url.pathname === '/big.pdf') {
		const body = Buffer.concat([Buffer.from('%PDF-1.4\n'), Buffer.alloc(6 * 1024 * 1024, 0x41)]);
		res.writeHead(200, { 'content-type': 'application/pdf', 'content-length': body.length });
		res.end(body);
		return;
	}

	if (url.pathname === '/v1/files') {
		const payload = JSON.stringify({
			type: 'error',
			error: { type: 'invalid_request_error', message: 'file exceeds the maximum allowed size' },
		});
		req.resume();
		req.on('end', () => {
			res.writeHead(400, { 'content-type': 'application/json' });
			res.end(payload);
		});
		return;
	}

	res.writeHead(404).end();
});

server.listen(PORT, () => console.log(`[mock] listening on http://127.0.0.1:${PORT}`));

2. Start n8n with binary data stored on disk:

N8N_DEFAULT_BINARY_DATA_MODE=filesystem \
N8N_AVAILABLE_BINARY_DATA_MODES=filesystem,default \
  pnpm start

3. Import the workflow below, run it, then open the Upload PDF to Files API node.

Example workflow
{
  "name": "NODE-5292 repro: POST binary upload error",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [0, 0],
      "id": "11111111-1111-1111-1111-111111111111",
      "name": "When clicking 'Execute workflow'"
    },
    {
      "parameters": {
        "url": "http://127.0.0.1:4599/big.pdf",
        "options": {
          "response": { "response": { "fullResponse": true, "responseFormat": "file" } }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [220, 0],
      "id": "22222222-2222-2222-2222-222222222222",
      "name": "Fetch example PDF file"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://127.0.0.1:4599/v1/files",
        "sendBody": true,
        "contentType": "binaryData",
        "inputDataFieldName": "data",
        "options": { "response": { "response": { "fullResponse": true } } }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [440, 0],
      "id": "33333333-3333-3333-3333-333333333333",
      "name": "Upload PDF to Files API"
    }
  ],
  "pinData": {},
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [[{ "node": "Fetch example PDF file", "type": "main", "index": 0 }]]
    },
    "Fetch example PDF file": {
      "main": [[{ "node": "Upload PDF to Files API", "type": "main", "index": 0 }]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Expected: the output panel shows the error box with file exceeds the maximum allowed size. Expand Error details → From HTTP Request and confirm Request.body reads Binary data got replaced with this text. Original was a stream. The browser console must stay clean.

To see the bug, check out master and repeat step 3: the panel shows only OUTPUT ⚠ and 1 item, and the console logs Converting circular structure to JSON from toDisplayString.

Also worth checking (multipart, the other upload shape): set Body Content Type to Form-Data, add a parameter of type n8n Binary File with input field data, and point it at the same URL. The panel must render its error too.

No regressions in the request preview: open any normal HTTP Request node and confirm the request shown under Error details is unchanged for JSON, string, and form-urlencoded bodies.

Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/NODE-5292

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)

🤖 PR Summary generated by AI

Review in cubic

A binary upload read from external binary storage stays a live stream. Once
the request is in flight, that stream's pipe chain is circular, and
`sanitizeUiMessage` only replaced Buffers, so the stream reached the browser
inside `error.context.request`. Execution data travels as `flatted`, which
restores the cycle, and interpolating it threw from Vue's `toDisplayString`.
The render error tore down the error view and left an empty output panel.

Replace upload streams in `sanitizeUiMessage` before `deepCopy` preserves the
cycle, covering `body`, a `form-data` instance, and the plain
`{ field: { value, options } }` map used below node version 4.2. Also render
sanitized copies in `NodeErrorView`, so an execution recorded before this fix
still shows its error.
@n8n-assistant

n8n-assistant Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

PR review overview

Based on ownership of the 4 changed files in this PR:

Ownership Files owned Share Source code Test files Misc
@n8n-io/nodes 2 50% +50 / -26 +67 / -0 +0 / -0
@n8n-io/frontend 2 50% +20 / -15 +33 / -1 +0 / -0
Total 4 100% +70 / -41 +100 / -1 +0 / -0

@BerniWittmann
BerniWittmann marked this pull request as ready for review August 24, 2026 12:36
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 317 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
editor-ui-esm 63.05MB 317 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: editor-ui-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/src-*.js 33 bytes 3.34MB 0.0%
assets/RunData-*.js 284 bytes 382.89kB 0.07%

Files in assets/RunData-*.js:

  • ./src/features/ndv/runData/components/error/NodeErrorView.vue → Total Size: 170 bytes

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 4 files

Architecture diagram
sequenceDiagram
    participant Store as Binary Storage (S3/Disk)
    participant Node as HTTP Request Node (Backend)
    participant API as External API
    participant UI as Frontend (NodeErrorView)

    Note over Store, API: Runtime Execution Flow (File Upload)

    Node->>Store: Request binary data reference
    Store-->>Node: Returns Readable Stream
    Note right of Store: Stream contains circular references<br/>(res -> socket -> _httpMessage)

    Node->>API: POST request (Stream as body/formData)
    API-->>Node: 4xx/5xx Error Response + JSON Body

    Note over Node: Error Preparation & Sanitization

    Node->>Node: Catch error and extract request metadata
    Node->>Node: CHANGED: Run sanitizeUiMessage()
    
    alt Body is Stream or Large Buffer
        Node->>Node: NEW: Replace with "Binary data got replaced..." string
    else Multipart (formData) contains Stream
        Node->>Node: NEW: Recursively replace nested Streams with placeholder
    end

    Node->>Node: Redact sensitive auth/headers
    Node-->>UI: Transport Execution Error (via flatted/JSON)

    Note over UI: UI Rendering (Node Details View)

    UI->>UI: NEW: map context/extra through replaceCircularReferences()
    Note right of UI: toRaw() used to skip reactive proxy overhead

    alt Rendering Error Details
        UI->>UI: Vue calls toDisplayString (JSON.stringify)
        Note right of UI: No longer throws TypeError on circularity
        UI-->>UI: Display sanitized Request and API Response
    end

    UI-->>UI: Render "Bad request" toast + Error Body panel
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/nodes-base/nodes/HttpRequest/GenericFunctions.ts Outdated
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...res/ndv/runData/components/error/NodeErrorView.vue 66.66% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

…ng them

`Object.entries` surfaces `__proto__` as an own key on anything `jsonParse`
builds, and assigning to that key runs the inherited setter: the key was
dropped from the copy and the throwaway object's prototype was retargeted
instead. `body` skips `deepCopy`, so it carried that out and lost the key from
the request shown in the error details.

`Object.fromEntries` defines each key rather than assigning it, restoring the
behaviour this branch had before the upload substitution went in.
@n8n-assistant n8n-assistant Bot added n8n team Authored by the n8n team node/improvement New feature or request labels Aug 24, 2026
@Joffcom
Joffcom requested a review from elsmr August 24, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed n8n team Authored by the n8n team node/improvement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant