{"record":{"id":"26db7edf59e19639","repo":"FlowiseAI/Flowise","slug":"subscription-is-canceled","errorCode":null,"errorMessage":"Subscription is canceled","messagePattern":"Subscription is canceled","errorType":"exception","errorClass":"InternalFlowiseError","httpStatus":401,"severity":"error","filePath":"packages/server/src/StripeManager.ts","lineNumber":58,"sourceCode":"    }\n\n    public async getProductIdFromSubscription(subscriptionId: string) {\n        if (!subscriptionId || subscriptionId.trim() === '') {\n            return ''\n        }\n\n        if (!this.stripe) {\n            throw new Error('Stripe is not initialized')\n        }\n\n        const subscriptionData = await this.cacheManager.getSubscriptionDataFromCache(subscriptionId)\n        if (subscriptionData?.productId) {\n            return subscriptionData.productId\n        }\n\n        try {\n            const subscription = await this.stripe.subscriptions.retrieve(subscriptionId)\n            if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')\n            const items = subscription.items.data\n            if (items.length === 0) {\n                return ''\n            }\n\n            const productId = items[0].price.product as string\n            await this.cacheManager.updateSubscriptionDataToCache(subscriptionId, {\n                productId,\n                subsriptionDetails: this.getSubscriptionObject(subscription)\n            })\n\n            return productId\n        } catch (error) {\n            return ''\n        }\n    }\n\n    public async getFeaturesByPlan(subscriptionId: string, withoutCache: boolean = false) {","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/FlowiseAI/Flowise/blob/abe4a8601a058047b350c260676826e21dd14101/packages/server/src/StripeManager.ts#L40-L76","documentation":"Inside getProductIdFromSubscription, after a cache miss the subscription is retrieved from Stripe. If its status is 'canceled', an InternalFlowiseError with HTTP 401 UNAUTHORIZED is thrown. This treats a canceled subscription as an authorization failure: the caller is not entitled to resolve a product for it. The surrounding try/catch swallows all errors and returns '' here, so in this specific method the throw is caught and converted to an empty string.","triggerScenarios":"Passing a subscription ID whose Stripe status is 'canceled' into getProductIdFromSubscription. This happens for ex-customers, after a plan cancellation completes, or when a webhook/job retries against a stale subscription ID.","commonSituations":"User cancelled their plan and a background job still references the old subscription. Test fixtures using a canceled test subscription. Race where cancellation webhook lands before an in-flight request completes.","solutions":["Stop calling billing-dependent APIs for subscriptions known to be canceled (filter by status upstream).","If an empty product response is acceptable, rely on the method's own catch (it returns '' on any error) rather than treating the throw as fatal in the caller.","Reconcile cached subscription status so canceled IDs are short-circuited before the Stripe retrieve.","For genuine re-activation flows, create a new subscription rather than reusing the canceled ID."],"exampleFix":"// before: canceled subscription throws, but caller may not expect it\nconst subscription = await this.stripe.subscriptions.retrieve(subscriptionId)\nif (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')\n\n// after: short-circuit on canceled status without a throw inside the swallowed try\nif (subscription.status === 'canceled') return ''","handlingStrategy":"try-catch","validationCode":"// Pre-check subscription status before relying on the product ID\nconst sub = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId)\nif (sub.status === 'canceled') {\n    // skip product-dependent logic\n    return ''\n}","typeGuard":"function isSubscriptionActive(status: Stripe.Subscription.Status): boolean {\n    return status !== 'canceled'\n}","tryCatchPattern":"// getProductIdFromSubscription swallows errors and returns '' here, so callers\n// should treat '' as 'unknown/canceled' rather than catching:\nconst productId = await stripeManager.getProductIdFromSubscription(subscriptionId)\nif (!productId) {\n    // handle canceled/unknown subscription without throwing","preventionTips":["Treat an empty product ID result from getProductIdFromSubscription as a canceled/unknown subscription.","Cache canceled status so repeated calls don't re-hit Stripe.","Filter canceled subscription IDs out of batch jobs.","Reconcile subscription status on cancellation webhooks."],"tags":["stripe","billing","subscription","authorization"],"backgroundTag":null,"analyzedSha":"abe4a8601a058047b350c260676826e21dd14101","analyzedAt":"2026-08-12T16:04:40.823Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}