Skip to content

[BUG] TypeScript Client Generator Incorrectly Handles File Arrays in multipart/form-data Requests #22597

@JerrySLau

Description

@JerrySLau

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

The typescript-fetch generator incorrectly generates code for multipart/form-data endpoints that accept arrays of files. The generated code has three critical bugs:

  1. Hardcodes useForm to false instead of using canConsumeForm, causing URLSearchParams to be used instead of FormData
  2. Converts File objects to strings using .join() instead of appending them individually to FormData
  3. Results in incorrect Content-Type (application/x-www-form-urlencoded instead of multipart/form-data)

This causes file uploads to fail because:

  • File objects are converted to the string [object File] instead of being sent as binary data
  • The wrong Content-Type header is sent, causing servers to reject the request
  • The request body format is incorrect for multipart/form-data

Impact: High severity - breaks all file array uploads in generated TypeScript clients.

openapi-generator version
openapi-generator-cli 7.17.0
  commit : 0120486
  built  : -999999999-01-01T00:00:00+18:00
  source : https://github.com/openapitools/openapi-generator
  docs   : https://openapi-generator.tech/

Is it a regression?: Unknown - not tested with previous versions.

OpenAPI declaration file content or url
openapi: 3.0.0
info:
  title: File Upload API
  version: 1.0.0
paths:
  /recording/upload:
    post:
      summary: Upload multiple recording files
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        '200':
          description: Upload successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  results:
                    type: array
                    items:
                      type: object

Minimal reproduction spec:

openapi: 3.0.0
info:
  title: Minimal File Array Upload Bug
  version: 1.0.0
paths:
  /upload:
    post:
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        '200':
          description: Success
Generation Details

CLI command:

openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-fetch \
  -o ./generated/typescript-fetch \
  --additional-properties=typescriptThreePlus=true,supportsES6=true

Config file (alternative):

{
  "generatorName": "typescript-fetch",
  "inputSpec": "openapi.yaml",
  "outputDir": "./generated/typescript-fetch",
  "additionalProperties": {
    "typescriptThreePlus": true,
    "supportsES6": true
  }
}

Language: TypeScript
Library: fetch API (browser)
Additional properties: None (or as shown above)

Steps to reproduce
  1. Create an OpenAPI spec with a multipart/form-data endpoint that accepts an array of files (see spec above)

  2. Generate TypeScript client:

    openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o ./output
  3. Use the generated client in browser code:

    import { DefaultApi } from './output';
    
    const api = new DefaultApi();
    const file1 = new File(['content1'], 'file1.mp3', { type: 'audio/mpeg' });
    const file2 = new File(['content2'], 'file2.mp3', { type: 'audio/mpeg' });
    
    // This will fail due to the bug
    await api.uploadRecordings({ files: [file1, file2] });
  4. Inspect the network request in browser DevTools:

    • Expected: Content-Type: multipart/form-data with binary file data
    • Actual: Content-Type: application/x-www-form-urlencoded with files=[object File]

Expected generated code:

const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: FormData;
let useForm = canConsumeForm;  // ✅ Use canConsumeForm
if (useForm) {
    formParams = new FormData();
} else {
    formParams = new URLSearchParams();
}

if (requestParameters['files'] != null) {
    // ✅ Append each file individually
    if (Array.isArray(requestParameters['files'])) {
        requestParameters['files'].forEach((file) => {
            formParams.append('files', file);
        });
    } else {
        formParams.append('files', requestParameters['files']);
    }
}

Actual generated code (with bug):

const canConsumeForm = runtime.canConsumeForm(consumes);
let formParams: { append(param: string, value: any): any };
let useForm = false;  // ❌ Hardcoded to false
if (useForm) {
    formParams = new FormData();
} else {
    formParams = new URLSearchParams();  // ❌ Wrong type
}

if (requestParameters['files'] != null) {
    // ❌ Converts File[] to string "[object File]"
    formParams.append('files', requestParameters['files']!.join(runtime.COLLECTION_FORMATS["csv"]));
}

Actual network request:

POST /recording/upload HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=UTF-8

files=[object File]
Related issues/PRs

Search keywords: typescript-fetch multipart file array, typescript-fetch FormData array, multipart/form-data array binary

Potential related issues (not verified):

  • Similar issues may exist for other generators (Java, Python, etc.)
  • Single file uploads work correctly (see comparison below)

Comparison with working single file upload:

In the same generated codebase, single file uploads are handled correctly:

// Working example for single file (from deprecated-ucenter/apis/DefaultApi.ts)
let formParams: { append(param: string, value: any): any };
let useForm = false;
useForm = canConsumeForm;  // ✅ Correctly assigns canConsumeForm
if (useForm) {
    formParams = new FormData();
} else {
    formParams = new URLSearchParams();
}

if (requestParameters['avatar'] != null) {
    formParams.append('avatar', requestParameters['avatar'] as any);  // ✅ Direct append
}

Note: The working example handles a single file correctly, but the same pattern is not applied to file arrays.

Suggest a fix

The generator template needs to be updated in two places:

1. Fix useForm assignment:

In the template that generates the form parameter handling code, change:

let useForm = false;

To:

let useForm = canConsumeForm;

2. Fix file array handling:

When the schema property is an array with items.format === 'binary', the generator should iterate and append each file individually instead of using .join():

if (requestParameters['files'] != null) {
    if (Array.isArray(requestParameters['files'])) {
        // For arrays of files, append each file individually
        requestParameters['files'].forEach((file) => {
            formParams.append('files', file);
        });
    } else {
        // For single files, append directly
        formParams.append('files', requestParameters['files']);
    }
}

3. Detection logic:

The generator should detect when:

  • consumes contains multipart/form-data
  • The schema property is an array type
  • The array items have format: binary

In such cases, it should:

  • Set useForm = canConsumeForm (not hardcode to false)
  • Use FormData instead of URLSearchParams
  • Iterate over the array and append each file individually

Template location: The fix should be in the typescript-fetch generator template files, likely in the form parameter generation logic.

Workaround for users:

Until the generator is fixed, developers can manually override the generated method:

async uploadRecordings(files: File[]): Promise<UploadRecordingsResponse> {
    const formData = new FormData();
    files.forEach((file) => {
        formParams.append('files', file);
    });

    const config = this.getConfig();
    const response = await fetch(`${config.baseUrl}/recording/upload`, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${token}`,
        },
        body: formData,
    });

    if (!response.ok) {
        throw new Error(`Upload failed: ${response.statusText}`);
    }

    const jsonValue = await response.json();
    return UploadRecordingsResponseFromJSON(jsonValue);
}

Additional Context:

  • Generator Template: typescript-fetch
  • OpenAPI Spec Source: TypeSpec → OpenAPI conversion
  • Runtime: Browser (fetch API)
  • Tested Browsers: Chrome, Safari

References:

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions