Summary
composio.files.upload() always fails on Node.js 22+ with TypeError: fetch failed (cause: InvalidArgumentError: invalid content-length header, UND_ERR_INVALID_ARG).
Root cause: uploadFileToS3 in ts/packages/core/src/utils/fileUtils.node.ts passes a manual Content-Length header to fetch() (line 217 on main). Node.js's built-in fetch (undici) rejects any request that has a user-supplied Content-Length header — undici computes it from the body itself and refuses to allow a manual one to avoid HTTP request smuggling. The PUT to the presigned R2 URL never leaves the client.
This affects every file_uploadable connector tool (Instagram media, Drive uploads, Slack file shares, etc.) when the host runtime is Node.js 22+ with built-in fetch.
Reproduction
Environment:
- Node.js v24.16.0 (also reproduced on v22.x)
@composio/core@0.10.0
- Manual call:
composio.files.upload({ file, toolSlug, toolkitSlug }) with a small File (e.g. 760 KB JPEG)
Minimal repro:
import { Composio } from '@composio/core'
const composio = new Composio({
apiKey: process.env.COMPOSIO_API_KEY,
dangerouslyAllowAutoUploadDownloadFiles: true,
})
// any reasonably-sized buffer (>~64KB seems to trigger consistently)
const bytes = new Uint8Array(800_000)
const file = new File([bytes.buffer], 'test.jpg', { type: 'image/jpeg' })
try {
await composio.files.upload({
file,
toolSlug: 'INSTAGRAM_UPLOAD_FILE',
toolkitSlug: 'INSTAGRAM',
})
} catch (err) {
console.log(err.name, err.message)
console.log('cause:', err.cause?.name, err.cause?.message, err.cause?.code)
}
Output:
TypeError fetch failed
cause: InvalidArgumentError invalid content-length header UND_ERR_INVALID_ARG
Note: With very small payloads (e.g. 1 KB) the request can sometimes succeed because undici's validation behavior differs at certain size boundaries, but anything close to a real-world image consistently fails.
Root cause (ts/packages/core/src/utils/fileUtils.node.ts)
// uploadFileToS3
const uploadResponse = await fetch(signedURL, {
method: 'PUT',
body: uploadBuffer,
headers: {
'Content-Type': mimeType,
'Content-Length': contentBytes.length.toString(), // ← offending line
},
});
Per the WHATWG Fetch spec and undici's implementation, Content-Length is a forbidden request header for user code — undici sets it automatically from the body. Supplying it explicitly results in InvalidArgumentError. See undici source: lib/web/fetch/headers.js (forbidden header check).
Suggested fix
Drop the manual Content-Length. fetch will compute it from uploadBuffer automatically:
const uploadResponse = await fetch(signedURL, {
method: 'PUT',
body: uploadBuffer,
headers: {
'Content-Type': mimeType,
- 'Content-Length': contentBytes.length.toString(),
},
});
I've validated this locally against @composio/core@0.10.0 (patched node_modules): with the line removed, R2 PUT succeeds, s3key is returned, and downstream connector calls (verified with INSTAGRAM_POST_IG_USER_MEDIA using image_file: { name, mimetype, s3key }) post successfully.
Happy to send a PR if useful.
Impact
Until this is fixed, any project that relies on staging files via the SDK on a modern Node runtime cannot use file_uploadable connector tools through the BE staging path. Workarounds we observed in production:
- Pass an HTTPS URL via
image_url (when the connector accepts URLs) — bypasses staging.
- Have the LLM use
COMPOSIO_REMOTE_WORKBENCH to upload the file inside Composio's sandbox — works but increases token cost and depends on the model being capable enough to drive that multi-step flow.
Neither of these are good defaults for production: the URL route requires public/signed URLs and isn't always available, and the workbench route is unreliable on smaller models.
References
Summary
composio.files.upload()always fails on Node.js 22+ withTypeError: fetch failed(cause:InvalidArgumentError: invalid content-length header,UND_ERR_INVALID_ARG).Root cause:
uploadFileToS3ints/packages/core/src/utils/fileUtils.node.tspasses a manualContent-Lengthheader tofetch()(line 217 onmain). Node.js's built-in fetch (undici) rejects any request that has a user-suppliedContent-Lengthheader — undici computes it from the body itself and refuses to allow a manual one to avoid HTTP request smuggling. The PUT to the presigned R2 URL never leaves the client.This affects every
file_uploadableconnector tool (Instagram media, Drive uploads, Slack file shares, etc.) when the host runtime is Node.js 22+ with built-in fetch.Reproduction
Environment:
@composio/core@0.10.0composio.files.upload({ file, toolSlug, toolkitSlug })with a smallFile(e.g. 760 KB JPEG)Minimal repro:
Output:
Note: With very small payloads (e.g. 1 KB) the request can sometimes succeed because undici's validation behavior differs at certain size boundaries, but anything close to a real-world image consistently fails.
Root cause (
ts/packages/core/src/utils/fileUtils.node.ts)Per the WHATWG Fetch spec and undici's implementation,
Content-Lengthis a forbidden request header for user code — undici sets it automatically from the body. Supplying it explicitly results inInvalidArgumentError. See undici source:lib/web/fetch/headers.js(forbidden header check).Suggested fix
Drop the manual
Content-Length.fetchwill compute it fromuploadBufferautomatically:const uploadResponse = await fetch(signedURL, { method: 'PUT', body: uploadBuffer, headers: { 'Content-Type': mimeType, - 'Content-Length': contentBytes.length.toString(), }, });I've validated this locally against
@composio/core@0.10.0(patchednode_modules): with the line removed, R2 PUT succeeds,s3keyis returned, and downstream connector calls (verified withINSTAGRAM_POST_IG_USER_MEDIAusingimage_file: { name, mimetype, s3key }) post successfully.Happy to send a PR if useful.
Impact
Until this is fixed, any project that relies on staging files via the SDK on a modern Node runtime cannot use
file_uploadableconnector tools through the BE staging path. Workarounds we observed in production:image_url(when the connector accepts URLs) — bypasses staging.COMPOSIO_REMOTE_WORKBENCHto upload the file inside Composio's sandbox — works but increases token cost and depends on the model being capable enough to drive that multi-step flow.Neither of these are good defaults for production: the URL route requires public/signed URLs and isn't always available, and the workbench route is unreliable on smaller models.
References
Content-Length: https://github.com/nodejs/undici