Skip to content

fix(downgrader): keep untyped multipart parts sent as octet-stream in 3.1 to 3.0 - #28

Merged
dinwwwh merged 2 commits into
mainfrom
claude/untyped-multipart-v3-conversion-b55e9b
Sep 30, 2026
Merged

dinwwwh merged 2 commits into
mainfrom
claude/untyped-multipart-v3-conversion-b55e9b

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

The 3.1 → 3.0 downgrader now keeps the content type of file parts in multipart and URL-encoded request bodies. In 3.1, a part with no type (the spec's way to describe raw binary) is sent as application/octet-stream, but 3.0 defines no default for untyped parts, so a downgraded upload field was left for each tool to guess. The converter now writes contentType: application/octet-stream into the part's Encoding Object, so 3.0 tools send the part exactly as 3.1 did.

Fixes

3.1 part 3.0 before 3.0 after
profileImage: {} no default content type encoding.profileImage.contentType: application/octet-stream
files: { type: array, items: {} } no default content type per item same, applied to each item
token: { type: string, contentEncoding: base64url } sent as text/plain application/octet-stream
  • Parts that already convert to a binary or byte string, such as contentMediaType: image/png, get the same explicit contentType, which restates 3.0's default. The output is therefore the same whether the body schema is inline or a $ref. Existing snapshots are unchanged.
  • An explicit contentType, or style / explode / allowReserved (which make the spec ignore contentType), is left as written. Existing headers are kept beside the added contentType.
  • Schemas are not tightened: the fix lives in the Encoding Object, not in the schema.
  • Responses and parameters are untouched, since encoding applies only to request bodies in 3.0 and 3.1.

For reviewers

  • Part types are looked up through $ref, allOf, anyOf, and oneOf. A part whose type is unknown (external or missing $ref, false, several types) and nested arrays are left alone rather than guessed.
  • { type: string, contentMediaType: image/png } is unchanged on purpose: 3.1.0 sends it as octet-stream while 3.1.1+ says text/plain, and the existing format: binary output matches the author's likely intent.
  • map() in shared.ts now passes each entry's key to its converter, so request body content can tell form media types apart.

Testing

  • 30 new cases: unit tests for each part shape, Encoding Object overrides, media type matching, shared objects, and cycles, plus an e2e test built from the spec's multipart examples and validated against the 3.0 schema.
  • The 17 cases that expect an added contentType fail on main.
  • pnpm test (517 tests), pnpm lint, and pnpm type:check pass.

… 3.1 to 3.0

In 3.1, a multipart or URL-encoded request body part with no `type`, or a
string with `contentEncoding`, defaults to `application/octet-stream`. 3.0
has no default for untyped parts and sends such strings as `text/plain`, so
a file part downgraded to 3.0 lost its content type. The converter now sets
`contentType: application/octet-stream` in the part's Encoding Object,
unless 3.0's defaults already give it or the Encoding Object sets a content
type or RFC6570-style fields.
@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ℹ️ Minor suggestions only.

Reviewed changes

  • Form-body detection: convertRequestMediaType routes multipart/* and application/x-www-form-urlencoded request bodies through a new finishFormMediaType; other request media types keep the old path. FORM_MEDIA_TYPE_FIELDS is a distinct copy of MEDIA_TYPE_FIELDS so a media type shared by a request and a response converts independently.
  • Part classification: subschemas/formParts gather top-level part schemas through $ref, allOf, anyOf, oneOf; defaultsToOctetStream decides whether 3.1.2 would default the part to application/octet-stream, and defaultsToOctetStreamIn30 whether the converted part already defaults there in 3.0.4.
  • shared.ts: map() now forwards each entry's key so the request-body converter can tell form media types apart.
  • Tests/README: 29 new unit and e2e cases (validated against the 3.0 schema) plus a README row.

I verified the load-bearing spec claims: 3.1.2 defaults a part with no type, or a string with contentEncoding, to application/octet-stream; 3.0.4 defaults a string with format: binary/byte to application/octet-stream. The classification logic matches both, and pnpm test, pnpm lint, pnpm type:check pass.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Comment thread packages/downgrader/src/v3.1-to-v3.0.ts Outdated
The 3.0-default check only saw properties declared directly on the body
schema, so a form behind a `$ref` got an explicit `contentType` for binary
parts while the same form inlined got none. Every part that 3.1 sends as
`application/octet-stream` now gets the explicit `contentType`, whether the
body schema is inline or a reference. What is sent does not change.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Removed the 3.0-default guard: 141b7f1 drops defaultsToOctetStreamIn30, so every form part whose 3.1 default is application/octet-stream now gets an explicit contentType, whether it is reached inline or through $ref/composition. This closes the inline-vs-reference inconsistency flagged in the prior review.
  • Documented the wider behavior: the README row no longer exempts parts that already convert to a binary/byte string, matching the emitted output.
  • Regression tests: a new case asserts the same encoding for inline and referenced body schemas, and the raw-binary / base64 / array-of-binary cases moved into the "sets contentType" group. I confirmed these fail with the old guard restored, so they are live coverage rather than theatre.

I re-ran the touched suites and repo checks on 141b7f1: pnpm test (downgrader unit + shared + e2e, 317 passed), pnpm lint, and pnpm type:check all pass. The explicit contentType restates 3.0.4's default for format: binary/byte parts, so the added output is redundant-but-consistent rather than a semantic change.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

@dinwwwh
dinwwwh merged commit 1e1e64b into main Sep 30, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant