Skip to content

[python] avoid etag/match_condition clientName collision with multiple etag headers#10816

Draft
l0lawrence wants to merge 1 commit into
microsoft:mainfrom
l0lawrence:fix/etag-multi-header-collision
Draft

[python] avoid etag/match_condition clientName collision with multiple etag headers#10816
l0lawrence wants to merge 1 commit into
microsoft:mainfrom
l0lawrence:fix/etag-multi-header-collision

Conversation

@l0lawrence
Copy link
Copy Markdown
Member

Problem

PR #10494 (which added support for custom etag wire names) broke operations that have more than one Azure.Core.eTag-typed header. The Azure Storage Blob copyFromUrl operation is a concrete example — it carries:

  • standard If-Match / If-None-Match
  • custom x-ms-source-if-match / x-ms-source-if-none-match

The emitter in http.ts stamps etagRole = "ifMatch" / "ifNoneMatch" onto every one of those headers. Then in preprocess/__init__.py, update_parameter blindly applies headers_convert(ETAG_MATCH_DATA) / ETAG_NONE_MATCH_DATA to every parameter with that role — overwriting clientName to "etag" and "match_condition" on both pairs. The operation ends up with two parameters named etag and two named match_condition.

The slot-picker in update_client already chose one ifMatch/ifNoneMatch slot per operation, but it did nothing to prevent the per-parameter rename on the others.

Fix

In preprocess/__init__.py update_client:

  1. Collect all ifMatch and ifNoneMatch candidates per operation, not just the first.
  2. New _pick_etag_slot helper prefers the standard If-Match / If-None-Match wire names over custom etag headers (matches pre-PR-10494 behaviour when both are present).
  3. Strip etagRole from non-selected candidates so update_parameter leaves their natural clientName intact (e.g. source_if_match, source_if_none_match).

Result

For copyFromUrl:

  • If-Match / If-None-Match → promoted to the etag / match_condition pair (unchanged behaviour vs. pre-PR-10494).
  • x-ms-source-if-match / x-ms-source-if-none-match → keep their natural source_if_match / source_if_none_match client names.

No more clientName collisions.

Tests

Adds tests/unit/test_preprocess_etag.py with six tests:

  • test_etag_role_preserved_when_only_standard_pair_present
  • test_etag_role_preserved_when_only_custom_pair_present
  • test_standard_etag_wins_over_custom_when_both_present (regression)
  • test_first_custom_pair_chosen_when_multiple_custom_pairs_present
  • test_synthetic_partner_still_works_with_only_one_custom_etag
  • test_full_update_yaml_does_not_collide_client_names (end-to-end)

Verified the three multi-etag tests fail on upstream/main and pass with this change. All existing unit tests still pass.

Spec referenced

The original repro is copyFromUrl in specification/storage/Microsoft.BlobStorage/routes.tsp of Azure/azure-rest-api-specs, which composes SourceIfMatchParameter, SourceIfNoneMatchParameter, IfMatchParameter, and IfNoneMatchParameter (all Azure.Core.eTag).

…ion with multiple etag headers

When an operation has more than one Azure.Core.eTag-typed header (e.g. Storage's copyFromUrl, which carries both standard If-Match/If-None-Match AND custom x-ms-source-if-match/x-ms-source-if-none-match), PR microsoft#10494's per-parameter conversion renamed every etag-typed header to clientName='etag' or 'match_condition', producing two parameters with the same name on the generated method.

Fix: in preprocess update_client, collect all ifMatch/ifNoneMatch candidates per operation, prefer the standard If-Match/If-None-Match wire names for the etag/match_condition slot, and strip etagRole from the non-selected candidates so they retain their natural clientName (e.g. source_if_match).

Adds tests/unit/test_preprocess_etag.py covering the standard-only, custom-only, mixed (regression), multi-custom-pair, synthetic-partner, and end-to-end clientName uniqueness scenarios.

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 27, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 27, 2026

Open in StackBlitz

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

commit: f4c7af2

@github-actions
Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @typespec/http-client-python
Show changes

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

Fix etag/match_condition clientName collision when an operation has more than one Azure.Core.eTag-typed header (e.g. Storage's copyFromUrl, which has both If-Match/If-None-Match and x-ms-source-if-match/x-ms-source-if-none-match). The standard If-Match/If-None-Match pair is now preferred for the etag/match_condition slot, and any additional etag-typed headers retain their natural client name (e.g. source_if_match).

@azure-sdk-automation
Copy link
Copy Markdown

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

Copy link
Copy Markdown
Contributor

@msyyc msyyc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor suggestions on the etag-slot picker logic.

fall back to the first candidate. Returns None if there are no candidates.
"""
if not candidates:
return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this early-return is redundant — iterating an empty list naturally falls through to return candidates[0], which would raise IndexError. Either drop the guard and rely on the loop (then handle the empty case at the call site), or keep it but note that candidates[0] is unreachable when empty. As-is it's just dead-code-ish.

# matches the pre-PR-10494 behaviour, and strip etagRole from the
# rest so they retain their natural clientName.
property_if_match = _pick_etag_slot(if_match_candidates, "if-match")
property_if_none_match = _pick_etag_slot(if_none_match_candidates, "if-none-match")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the standard wire-name literals 'if-match' / 'if-none-match' are passed from the call site. Consider promoting them to module-level constants (e.g. STANDARD_IF_MATCH_WIRE_NAME) for consistency with ETAG_MATCH_DATA / ETAG_NONE_MATCH_DATA already defined in this file, and to avoid a second source of truth.

c.pop("etagRole", None)
for c in if_none_match_candidates:
if c is not property_if_none_match:
c.pop("etagRole", None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case worth considering (not covered by tests): if an op has only a standard If-Match and only a custom x-ms-source-if-none-match (no standard If-None-Match and no custom If-Match partner), both will be selected and the custom source_if_none_match will be renamed to match_condition — pairing a standard header with a custom one under the same slot. Unlikely in practice, but worth either a defensive check (only promote a custom candidate if its standard counterpart was also selected, or vice versa) or an explicit comment documenting the chosen behavior.

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