Skip to content

fix(explorer): respect encoding.contentType for multipart/form-data parts#1369

Merged
sserrata merged 6 commits intomainfrom
fix/multipart-encoding-content-type
Mar 30, 2026
Merged

fix(explorer): respect encoding.contentType for multipart/form-data parts#1369
sserrata merged 6 commits intomainfrom
fix/multipart-encoding-content-type

Conversation

@sserrata
Copy link
Copy Markdown
Member

@sserrata sserrata commented Mar 30, 2026

Summary

When a multipart/form-data request body defines an OpenAPI encoding object with per-field contentType, those values were silently ignored. File parts always sent as application/octet-stream and string parts as text/plain regardless of the spec.

Changes:

  • buildPostmanRequest / setBody: thread encoding through and set contentType on each sdk.FormParam that declares one — fixes code snippets for text-type parts (e.g. metadata: application/json)
  • makeRequest: wrap file and string form parts in a typed Blob when encoding.contentType is present — fixes the actual HTTP request for all part types
  • Request/index.tsx: extract encoding from item.requestBody?.content?.[contentType]?.encoding and pass to both
  • CodeSnippets/index.tsx: accept requestBody prop (passed from ApiExplorer/index.tsx) and derive encoding from it so snippets stay in sync with the active content type
  • EncodingSelection/slice.ts (new Redux slice): tracks per-field content-type selection when encoding.contentType is a comma-separated list
  • FormBodyItem: renders a Content-Type dropdown for fields with multiple declared encoding options; dispatches selection into Redux
  • buildPostmanRequest empty-body case: preserves placeholder FormParams (with encoding applied) when no file has been uploaded yet, so the code snippet reflects the selected encoding immediately

encoding.contentType can be comma-separated per the OAS spec; the first value is used for the actual request, and a selector is shown in the UI when multiple are declared.

Known limitation — curl/Python snippets for binary file parts

The postman-code-generators curl codegen reads FormParam.contentType during preprocessing but does not emit ;type= for type === "file" params in the snippet — only for text-type params. As a result:

Surface Text parts (e.g. metadata: application/json) Binary file parts (e.g. file: image/png)
curl snippet ;type=application/json emitted ;type= missing (codegen bug)
Python snippet ❌ Uses file extension, ignores contentType ❌ Uses file extension, ignores contentType
Actual HTTP request ✅ Typed Blob ✅ Typed Blob

The actual HTTP request correctly uses the declared contentType for all part types. The snippet gap for binary file parts is an upstream library bug filed at postmanlabs/postman-code-generators#815.

Test plan

  • Actual HTTP request sends each part with the correct Content-Type — verify via HTTPBin (/anything/* endpoints echo the request back) or browser DevTools Network tab
  • curl snippet shows ;type=application/json for text-type parts with encoding.contentType: application/json
  • Fields without an encoding entry are unaffected (default browser behaviour)
  • Specs without an encoding object behave identically to before
  • When encoding.contentType is a comma-separated list, a Content-Type dropdown appears in the form body; selecting a value updates both the snippet and the actual request

Demo specs added under demo/examples/tests/multipartEncoding.yaml.

Closes #1247

🤖 Generated with Claude Code

…arts

OpenAPI encoding objects specify per-part Content-Type for multipart/form-data
but these were ignored, causing file parts to always send as
application/octet-stream regardless of the spec.

- setBody/buildPostmanRequest: thread encoding through and set contentType on
  sdk.FormParam for each field that declares one, so code snippets reflect
  the correct per-part Content-Type
- makeRequest: wrap file and string parts in a typed Blob when
  encoding.contentType is present so the actual HTTP request uses it
- Request/index.tsx: extract encoding from requestBody for the active
  content type and pass to both buildPostmanRequest and makeRequest
- CodeSnippets: accept requestBody prop and derive encoding from it so
  generated code snippets stay in sync

encoding.contentType may be comma-separated per OAS spec; the first value
is used.

Closes #1247
…ype support

Three endpoints covering:
- File part with explicit contentType
- JSON metadata + binary file (canonical #1247 use-case)
- Mixed parts (some with encoding, some without)

Servers point to HTTPBin so the actual request parts can be inspected
in the response body.

Related to #1247
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

Size Change: +7.23 kB (+0.33%)

Total Size: 2.22 MB

Filename Size Change
demo/.docusaurus/globalData.json 67.5 kB +1 kB (+1.51%)
demo/.docusaurus/registry.js 97.8 kB +1.44 kB (+1.49%)
demo/.docusaurus/routes.js 92.4 kB +1.33 kB (+1.46%)
demo/.docusaurus/routesChunkNames.json 38.3 kB +569 B (+1.51%)
demo/build/assets/js/main.********.js 662 kB +2.65 kB (+0.4%)
demo/build/assets/js/runtime~main.********.js 22.8 kB +247 B (+1.09%)
ℹ️ View Unchanged
Filename Size
demo/.docusaurus/codeTranslations.json 2 B
demo/.docusaurus/docusaurus.config.mjs 14.7 kB
demo/.docusaurus/i18n.json 372 B
demo/.docusaurus/site-metadata.json 1.51 kB
demo/build/assets/css/styles.********.css 164 kB
demo/build/index.html 97.6 kB
demo/build/petstore/add-pet/index.html 29.3 kB
demo/build/petstore/create-user/index.html 24 kB
demo/build/petstore/create-users-with-array-input/index.html 24.1 kB
demo/build/petstore/create-users-with-list-input/index.html 24.1 kB
demo/build/petstore/delete-order/index.html 23.8 kB
demo/build/petstore/delete-pet/index.html 24.1 kB
demo/build/petstore/delete-user/index.html 24.3 kB
demo/build/petstore/find-pets-by-status/index.html 24.8 kB
demo/build/petstore/find-pets-by-tags/index.html 25.4 kB
demo/build/petstore/get-inventory/index.html 23.1 kB
demo/build/petstore/get-order-by-id/index.html 24.1 kB
demo/build/petstore/get-pet-by-id/index.html 24.9 kB
demo/build/petstore/get-user-by-name/index.html 24.4 kB
demo/build/petstore/login-user/index.html 24.9 kB
demo/build/petstore/logout-user/index.html 23.7 kB
demo/build/petstore/new-pet/index.html 24.3 kB
demo/build/petstore/pet/index.html 22.5 kB
demo/build/petstore/place-order/index.html 23.3 kB
demo/build/petstore/schemas/apiresponse/index.html 24.6 kB
demo/build/petstore/schemas/cat/index.html 38.7 kB
demo/build/petstore/schemas/category/index.html 25.7 kB
demo/build/petstore/schemas/dog/index.html 39 kB
demo/build/petstore/schemas/honeybee/index.html 39.1 kB
demo/build/petstore/schemas/id/index.html 22.7 kB
demo/build/petstore/schemas/order/index.html 26.8 kB
demo/build/petstore/schemas/pet/index.html 38.5 kB
demo/build/petstore/schemas/tag/index.html 24.1 kB
demo/build/petstore/schemas/user/index.html 40.6 kB
demo/build/petstore/store/index.html 21.5 kB
demo/build/petstore/subscribe-to-the-store-events/index.html 30.2 kB
demo/build/petstore/swagger-petstore-yaml/index.html 30.2 kB
demo/build/petstore/update-pet-with-form/index.html 24.3 kB
demo/build/petstore/update-pet/index.html 24.7 kB
demo/build/petstore/update-user/index.html 24.3 kB
demo/build/petstore/upload-file/index.html 24.1 kB
demo/build/petstore/user/index.html 22.2 kB

compressed-size-action

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 30, 2026

Visit the preview URL for this PR (updated for commit df73f93):

https://docusaurus-openapi-36b86--pr1369-jc719vzu.web.app

(expires Wed, 29 Apr 2026 21:57:02 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: bf293780ee827f578864d92193b8c2866acd459f

sserrata and others added 3 commits March 30, 2026 16:22
…a encoding

When a multipart/form-data field declares a comma-separated encoding.contentType
(e.g. "image/png, image/jpeg, application/octet-stream"), a Content-Type
dropdown appears next to the field input. Selecting a type updates both the
generated code snippet and the actual request in real time.

- EncodingSelection/slice: new Redux slice storing per-field content type selections
- store/ApiItem: register slice and seed empty initial state
- FormBodyItem: parse comma-separated contentType, render FormSelect picker
  when multiple types are available, dispatch selection to Redux
- Body/index: extract encoding from spec and pass fieldEncoding to FormBodyItem
- Request/index + CodeSnippets: merge spec encoding with Redux encodingSelection
  so user picks propagate to both buildPostmanRequest and makeRequest
- multipartEncoding.yaml: add /post/multi-content-type demo endpoint with
  three selectable types to exercise the picker UI

Related to #1247
/post/* paths don't exist on HTTPBin — /anything/{path} accepts
any method and echoes the full request.
…aded

When the body is empty (no file selected), preserve the original
placeholder formdata params from the Postman request and apply the
selected encoding contentType to them. This ensures the code snippet
reflects the encoding selection immediately — before the user uploads
a file — since postman-code-generators emits `;type=<ct>` for FormParams
that have a contentType set.

Also wraps the Content-Type FormSelect in a div to push it onto its own
row below the property label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sserrata
Copy link
Copy Markdown
Member Author

Code Review: PR #1369fix(explorer): respect encoding.contentType for multipart/form-data parts

Closes #1247 | +385 / -20 | 11 files


Overview

This PR fixes a spec-compliance gap where OAS encoding.contentType declarations on multipart/form-data parts were silently ignored — all parts sent as application/octet-stream/text/plain regardless of the declared encoding. The fix threads encoding through the full request pipeline: code snippets, actual HTTP requests, and a new UI selector for comma-separated contentType lists.


Correctness

  • buildPostmanRequest / setBody: Correctly sets contentType on each sdk.FormParam using the first value from comma-separated lists. The spread ...(partContentType && { contentType }) pattern is clean.
  • makeRequest.ts: Wrapping form parts in typed Blob objects is the correct browser API to force a per-part Content-Type. Applies to both file and string parts.
  • Empty-body early return (buildPostmanRequest.ts:308): The new branch preserves placeholder FormParams with encoding applied before a file is selected — good UX. Mutates via param.contentType = ... but this is on an already-cloned deep copy, so it's safe.
  • Redux state initialization (ApiItem/index.tsx:162): encodingSelection: {} correctly seeds the store for persistence middleware.

Issues

Medium — Duplicate merge logic

The specEncoding extraction + user-selection merge is copy-pasted identically in Request/index.tsx and CodeSnippets/index.tsx:

// Duplicated in both files
const specEncoding = item.requestBody?.content?.[contentType]?.encoding ?? {};
const encodingSelection = useTypedSelector((state: any) => state.encodingSelection);
const encoding = Object.keys(specEncoding).length
  ? Object.fromEntries(Object.entries(specEncoding).map(...))
  : undefined;

This should be a shared custom hook (e.g., useResolvedEncoding(requestBody, contentType)) to avoid divergence.

Low — clearEncodingSelection is exported but never dispatched

EncodingSelection/slice.ts exports clearEncodingSelection but it is never called anywhere. If the user changes the active content type (e.g., from multipart/form-data to application/json) or resets the form, stale encoding selections will persist in the Redux store for that API item's lifetime. Consider dispatching this on content-type changes.

Low — useEffect with suppressed exhaustive-deps

FormBodyItem/index.tsx:

useEffect(() => {
  if (encodingOptions[0]) {
    dispatch(setFieldEncoding({ field: id, contentType: encodingOptions[0] }));
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

The eslint suppression is reasonable for a mount-only init, but encodingOptions is derived from the fieldEncoding prop — if the parent ever re-renders with a different fieldEncoding, the initial dispatch won't re-fire. Since this is a static spec value this is likely fine, but worth a comment explaining why.

Low — requestBody prop typed as any

CodeSnippets/index.tsx:

requestBody?: any;

Consistent with the existing state: any patterns in the file, but a narrower type (e.g., RequestBodyObject from the OpenAPI types) would prevent future misuse.

Low — No automated tests

The PR relies on the demo YAML + manual HTTPBin verification. Given the complexity of the encoding merge logic and the edge cases (empty body, comma-separated types, missing encoding), unit tests for buildPostmanRequest with encoding options and for the FormBodyItem dropdown behavior would strengthen confidence.


Style / Minor

  • The object type branch reformatting in FormBodyItem is a pure whitespace fix — fine, but unrelated to the feature.
  • The authOptions?.find(...) reformatting in CodeSnippets is also a pure style change.

Summary

Area Assessment
Core fix correctness ✅ Correct approach for all three surfaces
Known limitation documented ✅ Upstream codegen bug filed and linked
State management ⚠️ Stale state possible on content-type switch
Code duplication ⚠️ Merge logic duplicated in two components
Type safety ⚠️ requestBody: any and state: any (consistent with existing patterns)
Test coverage ❌ No automated tests added
FileArrayFormBodyItem Not updated — array file inputs won't get encoding applied in the UI

The core fix is sound and well-scoped. The main actionable items before merge are: extracting the duplicate merge logic into a hook, and wiring clearEncodingSelection on content-type switches to prevent stale state.

🤖 Generated with Claude Code

- Extract duplicate specEncoding+user-selection merge logic into a shared
  useResolvedEncoding(requestBody) hook; replaces copy-pasted blocks in
  Request/index.tsx and CodeSnippets/index.tsx
- Narrow requestBody prop type in CodeSnippets from `any` to RequestBodyObject
- Dispatch clearEncodingSelection when the active content type changes so
  stale per-field selections don't persist across content-type switches
- Add comment explaining the mount-only useEffect in FormBodyItem

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sserrata sserrata merged commit 5beabbe into main Mar 30, 2026
11 checks passed
@sserrata sserrata deleted the fix/multipart-encoding-content-type branch March 30, 2026 21:58
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.

Unable to set custom or auto detected "Content-Type" for File Parts in multipart/form-data

1 participant