{"record":{"id":"681f6793f01a773c","repo":"FlowiseAI/Flowise","slug":"subscription-is-canceled-681f67","errorCode":null,"errorMessage":"Subscription is canceled","messagePattern":"Subscription is canceled","errorType":"exception","errorClass":"InternalFlowiseError","httpStatus":401,"severity":"error","filePath":"packages/server/src/UsageCacheManager.ts","lineNumber":105,"sourceCode":"    }\n\n    public async getSubscriptionDetails(subscriptionId: string, withoutCache: boolean = false): Promise<Record<string, any>> {\n        const stripeManager = await StripeManager.getInstance()\n        if (!stripeManager || !subscriptionId) {\n            return UNLIMITED_QUOTAS\n        }\n\n        // Skip cache if withoutCache is true\n        if (!withoutCache) {\n            const subscriptionData = await this.getSubscriptionDataFromCache(subscriptionId)\n            if (subscriptionData?.subsriptionDetails) {\n                return subscriptionData.subsriptionDetails\n            }\n        }\n\n        // If not in cache, retrieve from Stripe\n        const subscription = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId)\n        if (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')\n\n        // Update subscription data cache\n        await this.updateSubscriptionDataToCache(subscriptionId, { subsriptionDetails: stripeManager.getSubscriptionObject(subscription) })\n\n        return stripeManager.getSubscriptionObject(subscription)\n    }\n\n    public async getQuotas(subscriptionId: string, withoutCache: boolean = false): Promise<Record<string, number>> {\n        const stripeManager = await StripeManager.getInstance()\n        if (!stripeManager || !subscriptionId) {\n            return UNLIMITED_QUOTAS\n        }\n\n        // Skip cache if withoutCache is true\n        if (!withoutCache) {\n            const subscriptionData = await this.getSubscriptionDataFromCache(subscriptionId)\n            if (subscriptionData?.quotas) {\n                return subscriptionData.quotas","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/FlowiseAI/Flowise/blob/abe4a8601a058047b350c260676826e21dd14101/packages/server/src/UsageCacheManager.ts#L87-L123","documentation":"UsageCacheManager.getSubscriptionDetails returns cached subscription details, falling back to a live Stripe retrieve on cache miss. If the retrieved subscription status is 'canceled', it throws InternalFlowiseError 401 before caching or returning. This is the cache layer's mirror of StripeManager's cancellation guard, so quota/feature code that reads subscription details also enforces entitlement.","triggerScenarios":"Any code path calling getSubscriptionDetails (directly or via quota/feature resolution) for a subscription Stripe reports canceled, after a cache miss forces the live retrieve.","commonSituations":"Background usage-sync job running against a just-canceled subscription. New deployment with an empty cache so the first request for a canceled sub hits Stripe. Cache eviction (TTL) expiring a previously-active entry after cancellation.","solutions":["Have cancellation webhooks write a canceled marker into the cache so subsequent lookups short-circuit without throwing.","Catch the 401 in callers and degrade to a disabled/free entitlement set.","Filter canceled subscription IDs out of periodic usage jobs.","Keep cached canceled status long-lived so the throw only happens once."],"exampleFix":"// before\nconst subscription = await stripeManager.getStripe().subscriptions.retrieve(subscriptionId)\nif (subscription.status === 'canceled') throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'Subscription is canceled')\n\n// after: cache + return a canceled detail object instead of throwing\nif (subscription.status === 'canceled') {\n    const details = stripeManager.getSubscriptionObject(subscription)\n    await this.updateSubscriptionDataToCache(subscriptionId, { subsriptionDetails: details })\n    return details\n}","handlingStrategy":"try-catch","validationCode":"// Pre-check cached/live status to avoid the throw inside getSubscriptionDetails\nconst cached = await cacheManager.getSubscriptionDataFromCache(subscriptionId)\nif (cached?.subsriptionDetails?.status === 'canceled') {\n    // degrade without triggering the live-retrieve throw\n    return cached.subsriptionDetails\n}","typeGuard":"function isSubscriptionActive(details: { status?: string } | undefined | null): boolean {\n    return !!details && details.status !== 'canceled'\n}","tryCatchPattern":"try {\n    const details = await cacheManager.getSubscriptionDetails(subscriptionId)\n} catch (err) {\n    if (err instanceof InternalFlowiseError && err.statusCode === StatusCodes.UNAUTHORIZED) {\n        // subscription canceled: degrade to a disabled state\n        return { status: 'canceled', active: false }\n    }\n    throw err\n}","preventionTips":["Write a canceled marker into the cache from cancellation webhooks so lookups short-circuit.","Catch the 401 in callers and degrade to a disabled entitlement set.","Exclude canceled subscription IDs from periodic usage jobs.","Use a long TTL for cached canceled status so the throw happens at most once."],"tags":["stripe","billing","subscription","cache","authorization","quotas"],"backgroundTag":null,"analyzedSha":"abe4a8601a058047b350c260676826e21dd14101","analyzedAt":"2026-08-12T16:04:40.823Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}