chargebee-typescript icon indicating copy to clipboard operation
chargebee-typescript copied to clipboard

[feature request] NextJS Edge Runtime Support

Open JanThiel opened this issue 1 year ago • 15 comments

Hey there,

in the modern times the "Edge" runtime gets more and more attention. It is a subset of the NodeJS API which makes it faster and more suitable for "Cloud Hosting" services like Vercel or Cloudflare Pages. In particular when building NextJS apps.

As hosting a custom Chargebee Portal / Checkout on one of the aforementioned services in combination with a NextJS app is quite a powerful approach, I would like to ask whether you are willing to make this library "Edge" compatible. There should only be some places where you have to switch packages or only use them conditionally.

There is this nice guide which explains the benefits of offering Edge support: https://vercel.com/guides/library-sdk-compatible-with-vercel-edge-runtime-and-functions

And some more compare-charts-docs explaining the Edge runtime compared to full NodeJS: https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes

And finally the Edge Runtime - as in "all the supported APIs" - for NextJS is documented here: https://nextjs.org/docs/app/api-reference/edge

Based on our current development the one problematic place is this: https://github.com/chargebee/chargebee-typescript/blob/2bc591b8abeeef17b42a012167c9e9d8cabeaf54/src/core.ts#L8

And it is just used for information Metadata... So not really necessary at all.

Thank you for reading and considering :-)

JanThiel avatar Mar 25 '24 19:03 JanThiel

Hey @JanThiel,

Thanks a lot for the detailed notes. Yes, we're considering making the SDK Edge compatible but we don't have a solid timeline for this yet unfortunately.

I will share updates here when we have more details & timeline for this.

cb-sriramthiagarajan avatar Mar 26 '24 13:03 cb-sriramthiagarajan

Hey @cb-sriramthiagarajan,

Thanks for your feedback.

From our current knowledge the os module is the only one preventing and Edge deployment. Will let you know once we have the app deployed.

We will use a fork of the library till you have some official release.

Best,

Jan

JanThiel avatar Mar 26 '24 19:03 JanThiel

It would have been to easy ;-) Further digging in, there - naturally - is more in the way to go...

http, https and the q module all are incompatible with Edge Runtime. The first two can easily be replaced by using fetch.

q uses setImmediate which is NodeJS only. Just FYI.

JanThiel avatar Mar 31 '24 17:03 JanThiel

Got that sorted out as well.

tldr: You have to raise the Min NodeJS Version of the package to 18.

Then you can use fetch instead of the http and https modules. The q library is obsolete as well, as you can use native Promises.

But to have the native web libraries in NodeJS version 18 is required. fetch is only part of Node since then.

You can find the changes - based on chargebee-node on our Fork ( https://github.com/Hive-IT-GmbH/chargebee-node ).

JanThiel avatar Mar 31 '24 18:03 JanThiel

Just as a note: The fork is running fine on Vercel and Cloudflare Pages.

JanThiel avatar May 21 '24 15:05 JanThiel

Hi @JanThiel, we have released a new beta version that supports Edge Runtime and will eventually be the single package to integrate Chargebee using JavaScript/TypeScript. It'd be great if you could try it and share your feedback.

You can install this using npm install chargebee@beta

cb-sriramthiagarajan avatar Aug 26 '24 07:08 cb-sriramthiagarajan

@cb-sriramthiagarajan Thank you very much for the update and your work. Looks like a massive improvement to me. Replaced the old version with the new one. Tested on Cloudflare Pages. Works well :-)

JanThiel avatar Aug 26 '24 10:08 JanThiel

That's great @JanThiel! Thanks for checking this quickly.

How much is your fork diverged from upstream? I wonder how easy / difficult would it be to switch from your fork to this package once we're out of beta? :)

cb-sriramthiagarajan avatar Aug 26 '24 10:08 cb-sriramthiagarajan

@cb-sriramthiagarajan I would say it's a straightforward task. Nothing complex nor surprising. The code gets cleaner. So there is a real benefit in migrating to 3.0. But still you have to change all library calls. I would say 3.0 is as close to a drop-in replacement for 2.x as you can come while solving the problems of the 2.x library foundations.

The most remarkable changes we encountered while migrating:

New Import

- import chargebee from "chargebee";
+ import Chargebee from "chargebee";

Setup as Object instantiation

- chargebee.configure({
-    site: process.env.CHARGEBEE_SITE,
-    api_key: process.env.CHARGEBEE_API_KEY
- });

+ const chargebee = new Chargebee({
+     site: process.env.CHARGEBEE_SITE,
+     apiKey: process.env.CHARGEBEE_API_KEY
+ });

Removal of the need to call .request()

- const subscription = await chargebee.subscription.retrieve(params.id).request();
+ const subscription = await chargebee.subscription.retrieve(params.id);

Typings are now much better and autocomplete as well. Naturally. In general it feels much better to work with 3.0 than the current lib version.

JanThiel avatar Aug 26 '24 12:08 JanThiel

How much is your fork diverged from upstream?

Close to none, Just patched the base library. Nothing changed that has any impact on the usage of the library. As such I simply merged your upstream changes into it with no issues.

JanThiel avatar Aug 26 '24 12:08 JanThiel

Awesome, you captured the major changes accurately @JanThiel 😄

I forgot to mention that we have a Migration guide for v3 and the README in the next branch also has more info on the usage.

Close to none, Just patched the base library. Nothing changed that has any impact on the usage of the library. As such I simply merged your upstream changes into it with no issues.

That's great to hear! We'll be testing this with a few customers to see if there are any issues before we publish this as the latest version. It'd be great if you could test this version in your test / staging environment and report any issues that you encounter. Thanks!

cb-sriramthiagarajan avatar Aug 26 '24 15:08 cb-sriramthiagarajan

Hey @cb-sriramthiagarajan,

we do face some issue on the local development. Although it runs fine on Cloudflare Pages, it does not locally. Neither on Windows nor on Mac.

We use NextJS 14.2.7 on NodeJS 20.15.1. pnpm 8.7.1 as package manager. All NextJS Endpoints are in edge mode.

Using the following call with the 3.0.0-beta1 triggers the error. Removing the call renders the app just fine. Switching back to the old forked version works fine as well.

    const subscription = await chargebee.subscription.list({"customer_id[is]": user['sub']});

This is the shortened Stacktrace with the error (RequestContentLengthMismatchError + UND_ERR_REQ_CONTENT_LENGTH_MISMATCH):

cause: RequestContentLengthMismatchError: Request body length does not match content-length header
      at write (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10734:41)
      at _resume (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10711:33)
      at resume (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10607:7)
      at connect (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10594:7) {
    code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'

And here the full error and stacktrace:

\node_modules\.pnpm\[email protected]_@[email protected]\node_modules\wrangler\wrangler-dist\cli.js:29768
            throw a;
            ^

Error: fetch failed
    at context.fetch (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\web\sandbox\context.js:292:38)        
    at fetch (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/react/cjs/react.react-server.development.js:189:16)
    at doOriginalFetch (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/lib/patch-fetch.js:387:24)
    at eval (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/lib/patch-fetch.js:536:24)
    at eval (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/lib/trace/tracer.js:115:36)
    at NoopContextManager.with (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/@opentelemetry/api/index.js:2:7062)
    at ContextAPI.with (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/@opentelemetry/api/index.js:2:518)
    at NoopTracer.startActiveSpan (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/@opentelemetry/api/index.js:2:18108)
    at ProxyTracer.startActiveSpan (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/@opentelemetry/api/index.js:2:18869)
    at eval (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/lib/trace/tracer.js:97:103)
    at NoopContextManager.with (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/@opentelemetry/api/index.js:2:7062)
    at ContextAPI.with (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/compiled/@opentelemetry/api/index.js:2:518)
    at NextTracerImpl.trace (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/lib/trace/tracer.js:97:28)
    at patched (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/lib/patch-fetch.js:180:75)
    at FetchHttpClient.fetchWithAbortTimeout (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected]/node_modules/chargebee/esm/net/FetchClient.js:50:30)
    at FetchHttpClient.makeApiRequest (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected]/node_modules/chargebee/esm/net/FetchClient.js:18:26)
    at eval (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected]/node_modules/chargebee/esm/RequestWrapper.js:70:55)
    at new Promise (<anonymous>)
    at RequestWrapper.request (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected]/node_modules/chargebee/esm/RequestWrapper.js:40:25)
    at RequestWrapper.getRequest (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected]/node_modules/chargebee/esm/RequestWrapper.js:15:25)
    at Object.eval [as list] (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected]/node_modules/chargebee/esm/createChargebee.js:28:27)
    at eval (webpack-internal:///(rsc)/./src/app/api/subscriptions/route.ts:26:55)
    at async eval (webpack-internal:///(rsc)/./node_modules/.pnpm/@[email protected][email protected]/node_modules/@auth0/nextjs-auth0/dist/helpers/with-api-auth-required.js:30:20)
    at async eval (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/future/route-modules/app-route/module.js:228:37)
    at async AppRouteRouteModule.execute (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/future/route-modules/app-route/module.js:157:26)
    at async AppRouteRouteModule.handle (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/future/route-modules/app-route/module.js:290:30)
    at async EdgeRouteModuleWrapper.handler (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/web/edge-route-module-wrapper.js:92:21)
    at async adapter (webpack-internal:///(rsc)/./node_modules/.pnpm/[email protected][email protected][email protected]/node_modules/next/dist/esm/server/web/adapter.js:179:16)
    at async \node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\web\sandbox\sandbox.js:110:22
    at async runWithTaggedErrors (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\web\sandbox\sandbox.js:107:9)
    at async DevServer.runEdgeFunction (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\next-server.js:1199:24)
    at async NextNodeServer.handleCatchallRenderRequest (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\next-server.js:248:37)
    at async DevServer.handleRequestImpl (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\base-server.js:812:17)
    at async \node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\dev\next-dev-server.js:339:20
    at async Span.traceAsyncFn (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\trace\trace.js:154:20)
    at async DevServer.handleRequest (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\dev\next-dev-server.js:336:24)
    at async invokeRender (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\lib\router-server.js:173:21)     
    at async handleRequest (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\lib\router-server.js:350:24)    
    at async requestHandlerImpl (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\lib\router-server.js:374:13)
    at async Server.requestListener (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\server\lib\start-server.js:141:13) {
  cause: RequestContentLengthMismatchError: Request body length does not match content-length header
      at write (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10734:41)
      at _resume (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10711:33)
      at resume (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10607:7)
      at connect (eval at requireWithFakeGlobalScope (\node_modules\.pnpm\[email protected][email protected][email protected]\node_modules\next\dist\compiled\edge-runtime\index.js:1:655888), <anonymous>:10594:7) {
    code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
  }
}

Node.js v20.15.1

This is the package.json:

{
  "name": "cb-test",
  "version": "0.3.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "setupDb": "npm run migrate",
    "migrate": "npx wrangler d1 migrations apply DB --local",
    "pages:build": "pnpm next-on-pages",
    "preview": "pnpm pages:build && wrangler pages dev",
    "deploy": "pnpm pages:build && wrangler pages deploy",
    "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
  },
  "engines": {
    "node": "^20.15.1"
  },
  "packageManager": "[email protected]",
  "type": "module",
  "dependencies": {
    "@auth0/nextjs-auth0": "^3.5.0",
    "@cloudflare/next-on-pages": "^1.13.2",
    "@fortawesome/fontawesome-pro": "^6.6.0",
    "@fortawesome/fontawesome-svg-core": "^6.6.0",
    "@fortawesome/free-brands-svg-icons": "^6.6.0",
    "@fortawesome/react-fontawesome": "^0.2.2",
    "@fortawesome/sharp-regular-svg-icons": "^6.6.0",
    "@shoelace-style/shoelace": "^2.16.0",
    "autoprefixer": "10.4.20",
    "chargebee": "3.0.0-beta.1",
    "copy-webpack-plugin": "^12.0.2",
    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.7",
    "kind-of": "^6.0.3",
    "lodash": "^4.17.21",
    "next": "14.2.7",
    "postcss": "8.4.41",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "server-only": "^0.0.1",
    "swr": "^2.2.5",
    "tailwindcss": "3.4.10",
    "typescript": "5.5.4",
    "wrangler": "3.72.3",
    "zod": "3.23.8"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20240821.1",
    "@tailwindcss/forms": "^0.5.7",
    "@types/kind-of": "^6.0.3",
    "@types/lodash": "^4.17.7",
    "@types/node": "^20.16.2",
    "@types/react": "18.3.3",
    "@types/react-dom": "18.3.0",
    "better-sqlite3": "^9.6.0",
    "eslint-plugin-next-on-pages": "^1.13.2"
  }
}

And this is the route.ts - you should be safe to remove the Auth0 stuff to reproduce:

import {NextRequest, NextResponse} from "next/server";
import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0/edge';
import Chargebee from "chargebee";

export const runtime = 'edge';

const chargebee = new Chargebee({
    site: process.env.CHARGEBEE_SITE,
    apiKey: process.env.CHARGEBEE_API_KEY
});

export const GET = withApiAuthRequired(async (request): Promise<Response> => {
    const response = new NextResponse();

    const session = await getSession(request,response);
    if (!session) {
        throw new Error(`Requires authentication`);
    }

    const { user } = session;
    //const subscription = {list: ["test","test2"]}; <-- This works
    const subscription = await chargebee.subscription.list({"customer_id[is]": user['sub']}); // <-- This breaks

    return NextResponse.json(subscription.list,{
        status: 200,
    });
});

JanThiel avatar Aug 28 '24 09:08 JanThiel

@cb-sriramthiagarajan This might be related: https://github.com/vercel/next.js/issues/53882

JanThiel avatar Aug 28 '24 09:08 JanThiel

Thanks for reporting this @JanThiel. We'll look into this and get back.

cb-sriramthiagarajan avatar Aug 28 '24 10:08 cb-sriramthiagarajan

Hi @JanThiel, we've released v3.0.0-beta.2 which fixes this issue. Can you give it a try please?

cb-sriramthiagarajan avatar Aug 29 '24 09:08 cb-sriramthiagarajan

Hi @JanThiel, we've released the new major version chargebee v3 which supports Edge runtime. Thanks for testing the beta version earlier. We can close this issue if this is working fine for you.

cb-sriramthiagarajan avatar Nov 06 '24 06:11 cb-sriramthiagarajan

Thank you @cb-sriramthiagarajan we already used the latest beta without any further issues!

JanThiel avatar Nov 06 '24 12:11 JanThiel