Skip to content

fix(http-client-python): preserve TypeSpec field order#10715

Draft
l0lawrence wants to merge 2 commits into
mainfrom
l0lawrence/investigate-multipart-form-bug
Draft

fix(http-client-python): preserve TypeSpec field order#10715
l0lawrence wants to merge 2 commits into
mainfrom
l0lawrence/investigate-multipart-form-bug

Conversation

@l0lawrence
Copy link
Copy Markdown
Member

@l0lawrence l0lawrence commented May 18, 2026

WIP Investigation ## Summary

The generated prepare_multipart_form_data helper in the Python emitter used
to emit all file parts before all data parts, regardless of how the
TypeSpec model declared them. This PR makes the helper preserve the
declaration order.

Why this is a bug

  • RFC 7578 §5.2 says "the parts are ordered and the order in which they
    are sent is significant."
  • The existing Spector payload.multipart scenario docs themselves expect
    declaration order — e.g. for ComplexHttpPartsModelRequest, the documented
    request body interleaves id, address, profileImage, previousAddresses, pictures, but the previous Python emitter would have rearranged that to
    profileImage, pictures, id, address, previousAddresses.
  • A real customer hit this on Azure AI Foundry's hosted-agents
    create_agent_version_from_code endpoint
    (Azure/azure-sdk-for-python#46932).
    Their server stream-parses the multipart body and expects the small JSON
    metadata parts before the binary file part; otherwise it reports the
    metadata as missing. They had to patch the generated helper via a
    post-emitter-fixes.cmd script to swap the loops.
  • C# (MultiPartFormDataBinaryContentDefinition_multipartContent.Add(...))
    and Java (MultipartFormDataHelper.addText/addFile) already preserve
    declared order. Python was the outlier.

Changes

Helper template (utils.py.jinja2)

Previously two separate loops — files first, then data — produced:

def prepare_multipart_form_data(body, multipart_fields, data_fields):
    for multipart_field in multipart_fields:
        ...                         # all files
    for data_field in data_fields:
        ...                         # all data, after every file

Now a single ordered list of (wire_name, is_file) tuples preserves
declaration order:

def prepare_multipart_form_data(body, fields: list[tuple[str, bool]]):
    for wire_name, is_file in fields:
        entry = body.get(wire_name)
        if is_file:
            if isinstance(entry, list):
                files.extend([(wire_name, e) for e in entry])
            elif entry:
                files.append((wire_name, entry))
        elif entry:
            files.append((wire_name, str(serialize_multipart_data_entry(entry))))

Call site (builder_serializer.py)

# before
file_fields = [p.wire_name for p in model_type.properties if p.is_multipart_file_input]
data_fields = [p.wire_name for p in model_type.properties if not p.is_multipart_file_input]
...
"_files = prepare_multipart_form_data(_body, _file_fields, _data_fields)",

# after
fields = [(p.wire_name, p.is_multipart_file_input) for p in model_type.properties]
...
"_files = prepare_multipart_form_data(_body, _fields)",

model_type.properties already iterates in TypeSpec declaration order, so
this is all that's needed to make the on-wire order match the spec.

Caveat (called out in the changelog)

This is observable on the wire for services whose TypeSpec model declares
data fields before file fields — that order is now preserved. The previous
"all files first" behavior was a bug, the new behavior matches the
TypeSpec contract and RFC 7578, and no published Azure service has been
identified as relying on the old order.

Validation

Regenerated the unbranded payload-multipart SDK locally and confirmed:

  • ComplexHttpPartsModelRequest now emits
    [("id", False), ("address", False), ("profileImage", True), ("previousAddresses", False), ("pictures", True)]
    — exactly matching the Spector scenarioDoc.
  • All 34 existing payload-multipart mock-API tests pass (sync + async).
  • 6 new unit tests in tests/unit/test_prepare_multipart_form_data.py
    cover declared-order preservation, list-valued file fields, JSON
    encoding of data fields, and falsy-entry skipping.
  • npm run lint (pylint 10.00/10) and black --check pass on the changed
    files.

…tipart_form_data

The generated prepare_multipart_form_data helper used to emit all file

parts before all data parts, regardless of how the TypeSpec model declared

them. RFC 7578 §5.2 says `the order in which [parts] are sent is

significant`, and some streaming server-side parsers (e.g. the Azure AI

Foundry hosted-agents `create_agent_version_from_code` endpoint) require

JSON metadata parts to precede binary file parts; otherwise they report the

metadata part as missing.

The helper now takes a single ordered list of `(wire_name, is_file)`

tuples and iterates in TypeSpec declaration order. The call site in

builder_serializer.py emits that list directly from `model_type.properties`

(which already iterates in declared order).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:python Issue for the Python client emitter: @typespec/http-client-python label May 18, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-python@10715

commit: 0987d04

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

All changed packages have been documented.

  • @typespec/http-client-python
Show changes

@typespec/http-client-python - fix ✏️

Generated prepare_multipart_form_data now serializes multipart fields in the,> order declared in the TypeSpec model, instead of emitting all file parts,> before all data parts. The previous behavior could break streaming server-side,> multipart parsers that require small JSON metadata parts to precede large,> binary file parts (per RFC 7578 §5.2), and it did not match the order,> documented in Spector multipart scenarios.,> ,> This is observable on the wire for services whose TypeSpec model declares data,> fields before file fields — that order is now preserved.,> ,> python,> # previously emitted call:,> _files = prepare_multipart_form_data(_body, _file_fields, _data_fields),> ,> # now emitted call (single ordered list of (wire_name, is_file) tuples):,> _files = prepare_multipart_form_data(_body, _fields),>

@azure-sdk-automation
Copy link
Copy Markdown

azure-sdk-automation Bot commented May 18, 2026

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@l0lawrence l0lawrence changed the title fix(http-client-python): preserve TypeSpec field order in prepare_multipart_form_data fix(http-client-python): preserve TypeSpec field order May 18, 2026
…ata to satisfy Azure pylint

Azure pylint guidelines (C4739/C4741/C4742) require :param/:return:/:rtype:

directives on any docstring. Convert the rationale to a leading comment so

the helper matches the rest of the file's style (none of the other helpers

have docstrings).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:python Issue for the Python client emitter: @typespec/http-client-python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants