Skip to content

@composio/core: drop manual Content-Length in S3 upload PUT (compat with strict undici as built-in fetch dispatcher; e.g. Next.js 16) #3548

@ThrouMisi

Description

@ThrouMisi

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

Metadata

Metadata

Labels

bugSomething isn't workingts

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions