Skip to content

fix(connections): preserve filename and content type in multipart file uploads#1611

Open
Dennis-UiPath wants to merge 2 commits intomainfrom
fix/multipart-preserve-filename
Open

fix(connections): preserve filename and content type in multipart file uploads#1611
Dennis-UiPath wants to merge 2 commits intomainfrom
fix/multipart-preserve-filename

Conversation

@Dennis-UiPath
Copy link
Copy Markdown

@Dennis-UiPath Dennis-UiPath commented May 4, 2026

Summary

ConnectionsService._build_activity_request_spec currently constructs
files[key] = (key, val, None) for every multipart parameter,
regardless of value type. Two consequences for any connector activity
that uploads a file:

  1. Filename is wrong. httpx interprets the first element of a
    3-tuple as the filename for Content-Disposition: form-data; name="…"; filename="…".
    Today every file is uploaded with the form-field name as its
    filename, e.g. attachment[file]. Downstream services that store
    attachments by name end up with literal attachment_file_. (Coupa
    sanitises [/], then appends . because there's no extension).
  2. Content-Type is None. httpx then defaults to application/octet-stream,
    so previewers / browsers cannot pick a sensible viewer.

The current source even flags the gap explicitly:

# files not supported yet supported so this will likely not work
files[key] = (
    key,
    val,
    None,
)  # probably needs to extract content type from val since IS metadata doesn't provide it

packages/uipath-platform/src/uipath/platform/connections/_connections_service.py#L791-L796

A real-world manifestation: every Coupa attachment uploaded by an agent using coupa.add_attachment lands as attachment_file_. instead of the document's real filename.

Fix

Branch on the value type to accept httpx's standard tuple shape, while keeping the existing bytes-only path working:

if isinstance(val, tuple):
    # Caller supplied httpx's (filename, content[, content_type])
    # shape — pass through verbatim. This is the recommended path
    # for file uploads.
    files[key] = val
elif isinstance(val, (bytes, bytearray)) or hasattr(val, "read"):
    # Raw file content with no filename — fall back to the form-field
    # name (legacy behaviour). Backwards compatible with callers that
    # still pass bytes directly.
    files[key] = (key, val, "application/octet-stream")
else:
    # Scalar — send as a plain multipart form field, not a file part.
    # The (None, value) shape tells httpx to omit `filename=...` from
    # the Content-Disposition.
    files[key] = (None, str(val))
Caller passes Wire-level result
("invoice.pdf", b"…", "application/pdf") (recommended) Content-Disposition: form-data; name="attachment[file]"; filename="invoice.pdf" + Content-Type: application/pdf
("invoice.pdf", b"…") (2-tuple shorthand) filename preserved, httpx infers content type
b"…" (legacy) unchanged behaviour: filename = form-field name
"some string" (scalar) sent as plain form field — filename=… is omitted

Backwards compatible: existing callers passing raw bytes keep working with no observable change on the wire.

Tests

Added TestMultipartFileUpload in packages/uipath-platform/tests/services/test_connections_service.py with four cases that inspect the serialized multipart body:

  • test_invoke_activity_multipart_tuple_3_preserves_filename — asserts filename="invoice.pdf" and Content-Type: application/pdf land in the wire body.
  • test_invoke_activity_multipart_tuple_2_preserves_filename — 2-tuple shorthand preserves filename.
  • test_invoke_activity_multipart_bytes_backwards_compatible — legacy raw-bytes callers keep behaviour: filename = form-field name, application/octet-stream content type.
  • test_invoke_activity_multipart_scalar_is_plain_form_field — non-file params (e.g. payload="{}") are emitted without a bogus filename= in Content-Disposition.

Test results

Ran from packages/uipath-platform/:

  • uv run pytest tests/services/test_connections_service.py57 passed (53 pre-existing + 4 new).
  • uv run pytest tests/1105 passed, 7 skipped (the skipped ones are pre-existing LLM integration tests gated on real credentials).
  • uv run ruff check src/uipath/platform/connections/_connections_service.py → clean.

Verification that the new tests catch the bug

I git stashed the fix and re-ran pytest …::TestMultipartFileUpload — 3 of the 4 new tests fail against the unpatched serializer (tuple_3, tuple_2, scalar). The bytes-backwards-compat test passes either way, because httpx defaults a None content type to application/octet-stream on the wire.

End-to-end validation

The downstream change in our agent that motivated this — using the patched SDK to send the real document filename to Coupa via add_attachment — was verified end-to-end against the staging Coupa instance: same byte payload as before, but the attachment is now stored with the real filename (invoice_<id>.pdf) instead of attachment_file_..

@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels May 4, 2026
…oads

`_build_activity_request_spec` always constructed
`files[key] = (key, val, None)` for multipart activities, which used the
form-field name (e.g. `attachment[file]`) as the multipart filename and
sent a `None` content type. Downstream services that store attachments
by filename ended up with literal names like `attachment_file_.` and no
extension.

Branch on the value type so callers can supply httpx's standard tuple
shape:

  * tuple                      -> passed through (recommended for files)
  * bytes / file-like          -> legacy fallback, key as filename,
                                  octet-stream content type (backwards
                                  compatible)
  * scalar (str/int/...)       -> plain multipart form field, no fake
                                  filename in Content-Disposition

The two pre-existing TODO comments
(`# files not supported yet supported so this will likely not work`
and the content-type note) are removed by the change.

Tests: 4 new cases under `TestMultipartFileUpload` in
`test_connections_service.py` exercise each branch by inspecting the
serialized multipart body. Verified that 3 of them fail against the
unpatched serializer (the bytes-fallback case passes either way because
httpx defaults a `None` content type to `application/octet-stream`).

`uv run pytest tests/` reports `1105 passed, 7 skipped` (pre-existing
LLM-integration skips that need real credentials), and `uv run ruff
check` is clean on the changed file.
Required by the `check-versions` CI gate so the multipart filename fix
can ship as a new release. Also bumps the `uipath-platform` floor in
`packages/uipath/pyproject.toml` so `uipath` pulls the fixed version.
@Dennis-UiPath Dennis-UiPath force-pushed the fix/multipart-preserve-filename branch from 30560ad to 7b86865 Compare May 6, 2026 07:21
@Dennis-UiPath Dennis-UiPath requested a review from radu-mocanu May 6, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant