Skip to content

Accept header defaults to */* for single-content-type responses (regression since v5.33.0 / PR #11019) #11070

Open

Description

@MikusMartinsons

Q&A (please complete the following information)

  • OS: N/A — not browser/OS-specific, this is a request-building logic bug
  • Browser: N/A (see above)
  • Version: N/A
  • Method of installation: npm dist assets, consumed transitively via darkaonline/l5-swagger (PHP), which bundles swagger-api/swagger-ui as a Composer/npm-mirrored dependency
  • Swagger-UI version: v5.33.0 (broken) / v5.32.6 (last known-good)
  • Swagger/OpenAPI version: OpenAPI 3.0

Content & configuration

Example OpenAPI definition (minimal repro shape — the key point is a response with exactly one content media type):

openapi: "3.0.0"
info:
  title: repro
  version: "1.0"
paths:
  /ping:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object

Swagger-UI configuration options: default, nothing special (no requestInterceptor, no custom responseInterceptor).

Describe the bug you're encountering

For an OAS3 operation whose response declares exactly one content media type (e.g. only application/json), clicking "Try it out" → Execute sends Accept: */* instead of Accept: application/json. This worked correctly through v5.32.6, and broke starting exactly at v5.33.0.

To reproduce...

  1. Load an OAS3 spec where an operation's response declares a single content type (see YAML above).
  2. Open that operation in Swagger UI. Note: the "Media type" <select> under each response code is present and correctly displays the single declared type (e.g. application/json) as selected — do not rely on this display value as evidence of correctness, see below.
  3. Click "Try it out" → "Execute" without changing anything.
  4. Inspect the outgoing request's Accept header — either via browser devtools Network tab, or more simply via the "Curl" snippet Swagger UI displays under the response after execution: it's generated from the actual resolved request (specSelectors.requestFor/mutatedRequestFor in live-response.jsx, not a separate live-preview), so unlike the "Media type" <select> it reliably reflects what was really sent — it's the easiest way to confirm this bug without opening devtools.

Expected behavior

Swagger UI should send Accept: application/json — the sole content type declared for the response — as it did through v5.32.6. Instead, starting at v5.33.0, it sends Accept: */*.

Screenshots

Not included

Additional context or thoughts

Root cause (traced in source): the Accept header is still built from the legacy, per-operation selector, unchanged since well before this regression:

// src/core/plugins/spec/actions.js (identical in both v5.29.1 and v5.33.0)
req.responseContentType = oas3Selectors.responseContentType(pathName, method) || "*/*"

oas3Selectors.responseContentType(path, method) reads requestData[path][method].responseContentType, which is only ever written by the UPDATE_RESPONSE_CONTENT_TYPE action (dispatched from responses.jsx's onResponseContentTypeChange, gated on controlsAcceptHeader).

PR #11019 ("add virtualization for schemas and operations", commit 0ad38e9c, released in v5.33.0) introduced a new, parallel, per-response-code content-type tracking system alongside the old one:

  • New action: UPDATE_RESPONSE_CODE_CONTENT_TYPE / setResponseCodeContentType (src/core/plugins/oas3/actions.js)
  • New reducer slice: requestData[path][method].responseCodeContentType[code] (src/core/plugins/oas3/reducers.js)
  • New selector: responseCodeContentType(path, method, code) (src/core/plugins/oas3/selectors.js)

And refactored src/core/components/response.jsx's _onContentTypeChange/getActiveContentType so that, for OAS3 specs, the dropdown's displayed/tracked value now primarily flows through this new per-code state:

// v5.33.0, src/core/components/response.jsx
_onContentTypeChange = (value) => {
  const { onContentTypeChange, controlsAcceptHeader, specSelectors, path, method, code, oas3Actions } = this.props
  if (specSelectors.isOAS3()) {
    oas3Actions.setResponseCodeContentType({ value, path, method, code })   // NEW slice
  } else {
    this.setState({ responseContentType: value })
  }
  onContentTypeChange({ value, controlsAcceptHeader })   // old callback, unchanged
}

The component that decides what's displayed was migrated to the new per-code model, but spec/actions.js's Execute logic — the code that actually builds the Accept header — was never updated to read from it. The two selectors have drifted apart, and the one actually used for the request header ends up unpopulated far more easily under the new component flow than before the refactor, producing the */* fallback even in the single-media-type case that previously worked out of the box with zero user interaction.

Suggested fix: in src/core/plugins/spec/actions.js, have the Accept header resolution fall back through both selectors, e.g.:

req.responseContentType =
  oas3Selectors.responseCodeContentType(pathName, method, /* current code */) ||
  oas3Selectors.responseContentType(pathName, method) ||
  "*/*"

(or equivalently, have onResponseContentTypeChange keep writing to both slices so they can't drift apart.)

Regression range (bisected by checking UPDATE_RESPONSE_CODE_CONTENT_TYPE's presence in src/core/plugins/oas3/actions.js at every intervening tag):

v5.29.1, v5.30.0-3, v5.31.0-2, v5.32.0-6:  absent — Accept header defaults correctly (11 releases checked individually)
v5.33.0:                                    present — Accept header defaults to */*

Workaround: pin to swagger-api/swagger-ui@5.32.6 until fixed.

Activity

  1. UmeshCode1 commented on Sep 29, 2026

    @UmeshCode1

    Excellent root cause analysis @MikusMartinsons.

    To expand on why this specifically broke when virtualization was introduced in #11019:

    The Lifecycle Disconnect

    In Swagger UI, an HTTP client cannot know what HTTP status code the server will ultimately return, so the Accept header is governed by the "accept controlling response" (the first 2xx or default response defined in the spec, as determined by getAcceptControllingResponse(responses)).

    Before virtualization was introduced in #11019:

    1. Responses and Response components always mounted immediately for the operation.
    2. In older component lifecycles, the controlling response would trigger onResponseContentTypeChange, writing the default media type into state.getIn(["requestData", path, method, "responseContentType"]).
    3. With virtualization in feat: add virtualization for schemas and operations #11019, responses and schemas are unmounted / lazily rendered to optimize DOM performance. Furthermore, <Response> only dispatches _onContentTypeChange when a user manually interacts with the <ContentType> dropdown.
    4. If a user simply clicks "Try it out" $\rightarrow$ "Execute" without touching the dropdown, requestData[path][method].responseContentType remains undefined.

    When execute in src/core/plugins/spec/actions.js executes:

    req.responseContentType = oas3Selectors.responseContentType(pathName, method) || "*/*"

    Because the user-interaction slice is empty, it falls straight through to */*.


    Recommended Fix: Spec-Derived Default Fallback

    Rather than attempting to synchronize component mount lifecycle events across virtualized views (which re-introduces rendering overhead), the resolution should be declarative and spec-derived.

    In src/core/plugins/spec/actions.js (around line 426) or in oas3Selectors.responseContentType:

    // Determine the media type from the accept-controlling response in the spec if not explicitly selected
    const responses = operation.get("responses")
    const acceptControllingResponse = getAcceptControllingResponse(responses)
    const defaultResponseContentType = acceptControllingResponse?.get("content")?.keySeq()?.first()
    
    req.responseContentType =
      oas3Selectors.responseContentType(pathName, method) ||
      defaultResponseContentType ||
      "*/*"

    Why this is resilient:

    1. Virtualization-proof: It does not rely on React lifecycle hooks (componentDidMount or onChange) firing in <Response>. Even if the responses table is completely unmounted or virtualized off-screen, execute resolves the correct default media type (application/json, etc.) directly from the immutable spec tree.
    2. Backwards Compatible: If a user does explicitly select a different media type in the dropdown, oas3Selectors.responseContentType(pathName, method) is populated and takes precedence.
    3. No Drift: Decouples request header generation from component rendering state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions