openapi-typescript-codegen: CancelablePromise fails to resolve with Next.js 13

Describe the bug I’ve tried to upgrade an application from Next.js 12 (v12.3.4) to 13 (v13.4.8) and suddenly experienced issues when fetching data from the API through the generated typescript client. It still send out the requests and I got a response from the API, but the Promise would not resolve and just stay stuck there. I did not see or receive error messages, but even after waiting minutes, there would never be a response.

Going into the generated code, it seemed to fail resolving in CancelablePromise.ts in the constructor trying to call the resolve function on the Promise. I troubleshooted with some console.logging to debug the issue, like the following (CancelablePromise.ts:40-50):

this.#promise = new Promise<T>((resolve, reject) => {
      this.#resolve = resolve;
      this.#reject = reject;

      const onResolve = (value: T | PromiseLike<T>): void => {
        console.log("starting to resolve");
        if (this.#isResolved || this.#isRejected || this.#isCancelled) {
          return;
        }
        console.log("resolving");
        this.#isResolved = true;
        this.#resolve?.(value);
        console.log("resolved");
      };

The console logs:

13:57:08.464 starting to resolve [CancelablePromise.ts:45:16]
13:57:08.465 resolving [CancelablePromise.ts:49:16]

This issue also seems to occur for reject.

OS: Linux Mint 21.1 Cinnamon Browser: Firefox 114.0.2

Tested also on a Mac with Chromium, which showed an error message on row 56:

this.#reject?.(reason);
TypeError: (intermediate value)(intermediate value)(intermediate value) is not a function

Now, I have no clue why this issue occurs when updating Next.js for me, but something in the background must’ve changed that I’m not aware of. The exact same generated code works for me with Next.js 12. It seems to try to call the resolve() function before the instance is initiated and not resolving the valid syntax of this.#resolve?.(value) correctly.

As said, no clue why this happens, but doing the undefined check without an intermediate value, seems to resolve this issue for me, so I would propose this in a PR and would be interested in your opinions on this.

So instead of this.#resolve?.(value);, I propose:

if (this.#resolve) this.#resolve(value);

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 19
  • Comments: 22 (6 by maintainers)

Commits related to this issue

Most upvoted comments

Update: i’ve upgraded to Next 14.0.3 and it seems like the bug is fixed there. Can anyone please confirm?

14.1.1 has this error, 14.1.2 solves it.

The related issue in swc was fixed today and it looks like it will be part of the next release. 🎉

@grikomsn thanks for sharing this. I am still new to JS land, probably stupid question but how would I call this?

I see you are exporting the patchCancellablePromise so could I execute this in my package.json ? Would love some hints, as I am stuck converting FastAPI Open API Speck to some typescript fetch client.

Reasonable request. Basically, you’d put everything in a Node.js file to run, instead of using the CLI.

In my case I used TypeScript, so ts-node.

First I created this file (fix-cancelable-promise.ts):

Click to view the code
// https://github.com/ferdikoomen/openapi-typescript-codegen/issues/1626#issuecomment-1675216771
import { CodeGenerator } from '@babel/generator'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import * as t from '@babel/types'
import * as fs from 'fs/promises'
import * as path from 'path'

export const fixCancellablePromise = async (dir: string) => {
  const src = path.resolve(process.cwd(), dir, 'core/CancelablePromise.ts')

  let content = await fs.readFile(src, { encoding: 'utf-8' })
  content = content.replaceAll('#', '')

  const ast = parse(content, {
    sourceType: 'module',
    plugins: ['typescript'],
  }) as any
  traverse(ast, {
    ClassMethod: (current) => {
      if (t.isIdentifier(current.node.key, { name: 'isCancelled' })) {
        current.remove()
        current.skip()
      }
    },
  })

  const { code } = new CodeGenerator(ast).generate()
  await fs.writeFile(src, code, 'utf-8')
}

Next make another file (like codegen.ts):

#!/usr/bin/env node

import * as openapi from 'openapi-typescript-codegen'
import * as path from 'path'
import * as fs from 'fs'
import { fixCancellablePromise } from './fix-cancelable-promise'

const run = async () => {
  const output = 'path/to/output/folder'
  await openapi.generate({
    // edit the other inputs here, just like the CLI
    input: `https://your-url.com/openapi.json`, // update this to match yours
    output,
  })

  fixCancelablePromise(output) // override the broken 
}

run()

Lastly, call the file:

npx ts-node codegen.ts

If you don’t want to use TypeScript you can write // @ts-nocheck at the top of the files if you get any warnings. Or you can rewrite it to use Node directly (a simple copy-paste into ChatGPT telling it to convert this code to pure Node.js will work). Then you can just run node codegen.js instead of npx ts-node codegen.ts.

I wonder if this could be an issue with SWC? @pitmullerIngka could you try to take different canaries between 13.4.7 and 13.4.8? It seems like that the issue might have been introduced in that span of releases.

More precisely, between v13.4.8-canary.9 and v13.4.8-canary.10