{"record":{"id":"a5cb6e3ac7c89c72","repo":"Stirling-Tools/Stirling-PDF","slug":"no-billing-portal-url-returned","errorCode":null,"errorMessage":"No billing portal URL returned","messagePattern":"No billing portal URL returned","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/proprietary/services/licenseService.ts","lineNumber":550,"sourceCode":"\n    const baseUrl = window.location.origin;\n    const returnUrl = `${baseUrl}/settings/adminPlan?seats_updated=true`;\n\n    const { data, error } = await supabase.functions.invoke(\"manage-billing\", {\n      body: {\n        return_url: returnUrl,\n        license_key: licenseKey,\n        self_hosted: true,\n        new_seat_count: newSeatCount,\n      },\n    });\n\n    if (error) {\n      throw new Error(`Failed to update seat count: ${error.message}`);\n    }\n\n    if (!data || !data.url) {\n      throw new Error(\"No billing portal URL returned\");\n    }\n\n    return data.url;\n  },\n};\n\n/**\n * Map license type to plan tier\n * @param licenseInfo - Current license information\n * @returns Plan tier: 'free' | 'server' | 'enterprise'\n */\nexport const mapLicenseToTier = (\n  licenseInfo: LicenseInfo | null,\n): \"free\" | \"server\" | \"enterprise\" | null => {\n  if (!licenseInfo) return null;\n\n  // No license or NORMAL type = Free tier\n  if (licenseInfo.licenseType === \"NORMAL\" || !licenseInfo.enabled) {","sourceCodeStart":532,"sourceCodeEnd":568,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/proprietary/services/licenseService.ts#L532-L568","documentation":"Thrown by updateEnterpriseSeats when the manage-billing invocation succeeded (no error) but the returned data has no url field. The function is expected to return a billing-portal URL for the user to confirm seat changes; an absent url means the response is malformed/degenerate even though no error was raised. Distinct from error 176: the call succeeded but the contract was broken.","triggerScenarios":"The manage-billing function returned 2xx with an empty/partial body; a refactor changed the response shape (e.g. url renamed to portal_url) and the client hasn't caught up; the function returned early without creating the session; Stripe returned no URL for an edge case the function didn't handle.","commonSituations":"Edge function version skew with the client; function silently returns {} on an unhandled branch; Stripe API change where the portal session object lacks url.","solutions":["Inspect the actual response body from manage-billing to see which field carries the URL.","Update the edge function to always return {url} on success, or update the client if the field was renamed.","Catch the error and surface a 'could not open billing portal' message with a retry.","Add a server-side log in manage-billing for the no-url branch."],"exampleFix":"// before\nif (!data || !data.url) throw new Error(\"No billing portal URL returned\");\n\n// after — tolerate the renamed field, else surface a clear error\nconst url = data?.url ?? data?.portal_url;\nif (!url) throw new Error(\"No billing portal URL returned\");\nreturn url;","handlingStrategy":"try-catch","validationCode":"// caller can't pre-validate response shape; ensure edge function returns {url} on success","typeGuard":null,"tryCatchPattern":"try {\n  const url = await licenseService.updateEnterpriseSeats(newCount, key);\n  window.location.href = url;\n} catch (e) {\n  if (/No billing portal URL/.test(e instanceof Error ? e.message : \"\")) {\n    showToast(t('license.portalUrlMissing'));\n  } else throw e;\n}","preventionTips":["Keep the manage-billing response shape under test so url is always returned on success.","Log the no-url branch server-side for diagnosis.","Version-skew the client when the edge function's response field is renamed."],"tags":["supabase","stripe","license","enterprise","seats","data-integrity","proprietary","typescript"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}