openapi-typescript-codegen icon indicating copy to clipboard operation
openapi-typescript-codegen copied to clipboard

(FormData) TypeError: source.on is not a function

Open kumboleijo opened this issue 1 year ago • 18 comments

What is the problem?

I'm using openapi-typescript-codegen for creating an axios client. When I try to call one of the services I end up getting the following error:

.../_nestjs/node_modules/delayed-stream/lib/delayed_stream.js:33
  source.on('error', function() {});
         ^
TypeError: source.on is not a function
    at Function.DelayedStream.create (.../_nestjs/node_modules/delayed-stream/lib/delayed_stream.js:33:10)
    at FormData.CombinedStream.append (.../_nestjs/node_modules/combined-stream/lib/combined_stream.js:45:37)
    at FormData.append (.../_nestjs/node_modules/form-data/lib/form_data.js:75:3)
    at process (.../_nestjs/client/app/src/generated/core/request.ts:117:26)
    at .../_nestjs/client/app/src/generated/core/request.ts:127:40
    at Array.forEach (<anonymous>)
    at .../_nestjs/client/app/src/generated/core/request.ts:127:27
    at Array.forEach (<anonymous>)
    at getFormData (.../_nestjs/client/app/src/generated/core/request.ts:125:14)
    at .../_nestjs/client/app/src/generated/core/request.ts:294:41

Like the stacktrace shows the error is thrown inside request.ts file in the getFormData function.

export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
    if (options.formData) {
        const formData = new FormData();

        const process = (key: string, value: any) => {
            if (isString(value) || isBlob(value)) {
                formData.append(key, value); // 🚨 this is where it breaks (isBlob == true)
            } else {
                formData.append(key, JSON.stringify(value));
            }
        };

        Object.entries(options.formData)
            .filter(([_, value]) => isDefined(value))
            .forEach(([key, value]) => {
                if (Array.isArray(value)) {
                    value.forEach(v => process(key, v));
                } else {
                    process(key, value);
                }
            });

        return formData;
    }
    return undefined;
};

Reproducable example

❯ node -v
v20.10.0

package.json

{
  "name": "api",
  "version": "0.0.1",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "prebuild": "rm -rf dist",
    "gen": "openapi -i openapi.json -o src/generated -c axios --useOptions --useUnionTypes",
    "build": "tsc"
  },
  "dependencies": {
    "axios": "^1.6.2",
    "form-data": "^4.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.10.4",
    "openapi-typescript-codegen": "0.25.0",
    "ts-node": "^10.9.2",
    "tsc": "2.0.4",
    "typescript": "^5.3.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "declaration": true,
    "removeComments": false,
    "emitDecoratorMetadata": true,
    "allowSyntheticDefaultImports": true,
    "incremental": false,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "noFallthroughCasesInSwitch": false,
    "lib": ["ES2022", "dom"],
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": false,
    "skipLibCheck": true,
    "baseUrl": "./",
    "rootDir": "./",
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

My openapi spec is looking like this:

{
  "openapi": "3.0.0",
  "paths": {
    "/mail/send": {
      "post": {
        "operationId": "sendMail",
        "summary": "Send a generic mail",
        "parameters": [
          { "name": "subject", "required": true, "in": "query", "schema": { "type": "string" } },
          { "name": "recipients", "required": true, "in": "query", "schema": { "type": "array", "items": { "type": "string" } } },
          { "name": "displayName", "required": true, "in": "query", "schema": { "type": "string" } },
          { "name": "replyTo", "required": true, "in": "query", "schema": { "type": "string" } },
          { "name": "ccs", "required": false, "in": "query", "schema": { "type": "array", "items": { "type": "string" } } },
          { "name": "bccs", "required": false, "in": "query", "schema": { "type": "array", "items": { "type": "string" } } }
        ],
        "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/SendGenericMailBodyDto" } } } },
        "responses": { "201": { "description": "" } },
        "tags": ["Mail"],
        "security": [{ "JWT": [] }]
      }
    }
  },
  "info": { "title": "API" },
  "tags": [],
  "servers": [],
  "components": {
    "schemas": {
      "SendGenericMailBodyDto": { "type": "object", "properties": { "attachments": { "type": "array", "items": { "type": "string", "format": "binary" } } }, "required": ["attachments"] }
    }
  }
}

The generated code for that service looks like this:

...
export class MailService {

    /**
     * Send a generic mail
     * @returns any
     * @throws ApiError
     */
    public static sendMail({
        subject,
        recipients,
        displayName,
        replyTo,
        formData,
        ccs,
        bccs,
    }: {
        subject: string,
        recipients: Array<string>,
        displayName: string,
        replyTo: string,
        formData: SendGenericMailBodyDto,
        ccs?: Array<string>,
        bccs?: Array<string>,
    }): CancelablePromise<any> {
        return __request(OpenAPI, {
            method: 'POST',
            url: '/mail/send',
            query: {
                'subject': subject,
                'recipients': recipients,
                'displayName': displayName,
                'replyTo': replyTo,
                'ccs': ccs,
                'bccs': bccs,
            },
            formData: formData,
            mediaType: 'multipart/form-data',
        });
    }

}

Now when I try to call that service the error occurs:

import * as MAIL from '..';

export async function sendMail() {
  const obj = { hello: 'world' };
  const blob = new Blob([JSON.stringify(obj, null, 2)], {
    type: 'application/json',
  });

  await MAIL.MailService.sendMail({
    displayName: 'Test',
    recipients: ['[email protected]'],
    subject: 'Test',
    replyTo: '[email protected]',
    formData: {
      attachments: [blob],
    },
  });
}

Am I missing something?

kumboleijo avatar Dec 12 '23 15:12 kumboleijo

@kumboleijo Did you ever figure out what caused this issue?

melanki avatar Jan 08 '24 15:01 melanki

@melanki sadly no. But what I can say is, that it is working in browser based environments (React App built with vite). Not sure if I'm doing something wrong in my typescript setup and mess up with ESM and CJS?

kumboleijo avatar Jan 12 '24 06:01 kumboleijo

I have also had this exact same issue when trying to use the generated code to upload a file. For now I have had to build my own request using axios. I am using the client in my e2e Playwright tests for creating test data.

I can provide more context if and examples if you are looking into this issue.

Thanks

costa-collibra avatar Jan 18 '24 17:01 costa-collibra

@costa-collibra Would you mind sharing some more insights on this and what exactly you had to tweak in your custom request in order to get it work? 🙏

kumboleijo avatar Jan 18 '24 18:01 kumboleijo

@kumboleijo My choice of words were probably a bit poor. In order to get it to work we couldnt use the library so we used axios directly.

export const uploadZip = async ({ filePath, fileName }: { filePath: string; fileName: string }): Promise<any> => {
  const data = new FormData();
  const setCustomAuthHeader = btoa(
    `${envConfig().CUSTOM_ADMIN_TEST_USERNAME}:${envConfig().CUSTOM_ADMIN_TEST_PASSWORD}`,
  );

  data.append('file', fs.createReadStream(filePath));
  data.append('fileName', fileName);

  const config = {
    method: 'post',
    maxBodyLength: Infinity,
    url: envConfig().BASE_URL + '/rest/2.0/workflowDefinitions',
    headers: {
      Authorization: `Basic ${setCustomAuthHeader}`,
      ...data.getHeaders(),
    },
    data: data,
  };

  const response = await axios.request(config);
  console.log(`RESPONSE: ${JSON.stringify(response.data)}`);

  return response;
};

This is why I think the issue resides in this library as this code is working.

I really hope this gets fixed though so we can go back to library for this endpoint.

costa-collibra avatar Jan 19 '24 08:01 costa-collibra

@costa-collibra oh I see... I thought you provided your own custom request file using:

npx openapi-typescript-codegen --input ./spec.json --output ./generated --request ./request.ts

  • https://github.com/ferdikoomen/openapi-typescript-codegen/wiki/Custom-request-file

I guess that could be a good workaround for you since you already have custom code written for that call.

kumboleijo avatar Jan 19 '24 08:01 kumboleijo

@costa-collibra oh I see... I thought you provided your own custom request file using:

npx openapi-typescript-codegen --input ./spec.json --output ./generated --request ./request.ts

  • https://github.com/ferdikoomen/openapi-typescript-codegen/wiki/Custom-request-file

I guess that could be a good workaround for you since you already have custom code written for that call.

I didnt even know this was possible, hopefully its only a temp workaround

costa-collibra avatar Jan 19 '24 08:01 costa-collibra

I am also experiencing this issue. In another browser based project, I was able to get this working, but I built an Electron app with electon-react-boilerplate and see the issue here.

I also just directly used Axios and made my own FormData() and see no issues with that.

mluogh avatar Jan 20 '24 02:01 mluogh

@jordanshatford we have this fixed in @hey-api/openapi-ts, right?

mrlubos avatar Mar 29 '24 12:03 mrlubos

@jordanshatford we have this fixed in @hey-api/openapi-ts, right?

Oh really. I will check this out asap.

costa-collibra avatar Mar 29 '24 12:03 costa-collibra

@costa-collibra let me know please, I'm not sure as I never ran into this with my Axios client

mrlubos avatar Mar 29 '24 12:03 mrlubos

@costa-collibra if it's not fixed, we have a whole issue around a slew of similar bugs https://github.com/hey-api/openapi-ts/issues/29

mrlubos avatar Mar 29 '24 13:03 mrlubos

Leave it with me and il get back to you

costa-collibra avatar Mar 29 '24 13:03 costa-collibra

@costa-collibra @mrlubos it has been fixed in the node client, working on updating the axios client to be fixed aswell. Should be in the next release

jordanshatford avatar Mar 29 '24 21:03 jordanshatford

@jordanshatford this is fantastic thanks for the update

for clarity have you fixed it in this https://github.com/hey-api/openapi-ts

costa-collibra avatar Mar 29 '24 21:03 costa-collibra

Check out our fork of this repository @hey-api/openapi-ts. We have fixed this issue in v0.32.1. If you run into any further issues, open an issue in our repository. Thanks.

NOTE: this is now fixed for both node and axios clients. No other clients experience this issue.

jordanshatford avatar Mar 30 '24 23:03 jordanshatford

Oh I didn't know about that fork / project 😱 Will check it out 🤝 Thanks for the hint! @jordanshatford @costa-collibra @mrlubos

kumboleijo avatar Apr 01 '24 16:04 kumboleijo

You're welcome @kumboleijo. If anyone in this thread still has to use a custom request file, please let us know why and how you use it as we're trying to discourage this pattern

mrlubos avatar Apr 01 '24 17:04 mrlubos