Repository navigation
fix(connectors): fetch-and-attach real multipart file parts (#645) - #818
Merged
keysersoft merged 5 commits intoOct 3, 2026
Merged
Conversation
…-ai#645) form-data bodies had no way to carry an actual file: appendFormParam coerced every value to a string, so a tool meant to upload an image (e.g. Etsy's uploadListingImage) sent the literal URL text as a form field instead of a real file part. Add a __file bodyMapping marker (bodyMapping: { image: { __file: "$image" } }), following the existing __raw/__spread convention. When the form-data branch sees it, it fetches the URL server-side through the existing SSRF guard, buffers it (capped by MAX_FILE_UPLOAD_BYTES, default 10MB, aborted early once crossed), and attaches it as a real multipart part with filename/content-type. Content-Disposition's filename wins when present; both it and the URL-derived fallback are sanitized the same way. Non-2xx responses from the file URL fail the tool call instead of attaching an error page as the file. The download is a bare request with none of the connector's own auth/headers/proxy config attached. form-urlencoded can't carry a file, so the marker is a config error there instead of being silently stringified. A form-data body backed by a file Buffer can only be read once; the OAuth2/LOGIN_TOKEN 401 auto-retry now rebuilds it from a small factory instead of resending the drained body. Frontend: a 'file' param type in the custom tool builder, selectable only when target is body and encoding is multipart/form-data. It emits the __file marker and is exposed to the model as { type: 'string', format: 'uri' }, since JSON Schema has no file type.
Replaces the plain axios call + manual stream-cap in fetchFileForUpload with fetchOutbound() (HelpCode-ai#821), which already covers the SSRF check on every redirect hop, the size cap, the timeout and non-2xx handling. Filename resolution now uses the post-redirect finalUrl rather than the original URL, so a short link resolves to the real file's name. Tests mock fetchOutbound directly rather than the raw axios call.
rangarajan19
force-pushed
the
fix/645-multipart-file-upload
branch
from
October 3, 2026 07:21
30e69e2 to
db04693
Compare
rangarajan19
marked this pull request as ready for review
October 3, 2026 07:22
added 3 commits
October 3, 2026 18:11
… left out
A $param resolves to the caller's value as-is, objects included, and
__spread lets the caller choose the keys. So a model could make any
form-data tool download a URL of its choosing by sending
{ "__file": "https://..." }. The marker now counts only where the
tool's own bodyMapping writes it; one arriving in the arguments fails the
call. A declared file the caller did not pass is left out of the form.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #645.
Draft — the download currently uses a plain
axioscall, isolated in its ownfetchFileForUploadfunction. Per the discussion on #645, I'll swap it to the shared outbound-fetch helper once it's onmain, before marking this ready for review.What this does
__filebodyMapping marker (bodyMapping: { image: { "__file": "$image" } }), following the existing__raw/__spreadconvention.form-databranch, detects that marker and fetches the URL server-side (through the existingassertSafeOutboundUrlSSRF guard, no new surface) instead of stringifying it, then attaches it as a real multipart part.form-urlencoded+__fileis a config error — that encoding can't carry a file.fileparam type in the custom tool builder, selectable only when target isbodyand encoding ismultipart/form-data. Exposed to the model as{ type: 'string', format: 'uri' }, since JSON Schema has no file type.Following the maintainer's corrections from the issue thread
FormDatabuilt from a live stream can't be read twice, andexecute()resends the sameaxiosConfigon an OAuth2/LOGIN_TOKEN 401 retry. The form is now built via a small factory and rebuilt for that retry, so the file is fetched once but the body can be resent intact.Content-Disposition's filename wins when present, otherwise the URL's last path segment; both go through the same sanitizing ([A-Za-z0-9._-], fallbackfile).application/octet-stream.MAX_FILE_UPLOAD_BYTESenv var, default 10MB, download aborted as soon as the cap is crossed.Tests
Added to
rest.engine.spec.ts:__fileURL and attaches it as a real multipart part, with no connector credentials on the download__filemarker when encoding is form-urlencoded__fileURL that resolves to a blocked address (SSRF)82/82 tests pass (76 existing + 6 new). Full backend + frontend typecheck clean.
Docs
Short note in
docs/tool-definition.mddocumenting the__filemarker.