{"record":{"id":"cdde18d2f040c296","repo":"langgenius/dify","slug":"the-workspace-custom-feature-has-reached-the-limit","errorCode":null,"errorMessage":"The workspace custom feature has reached the limit of your subscription.","messagePattern":"The workspace custom feature has reached the limit of your subscription\\.","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"api/controllers/console/wraps.py","lineNumber":214,"sourceCode":"            if features.billing.enabled:\n                members = features.members\n                apps = features.apps\n                documents_upload_quota = features.documents_upload_quota\n                annotation_quota_limit = features.annotation_quota_limit\n                if resource == \"members\" and 0 < members.limit <= members.size:\n                    abort(403, \"The number of members has reached the limit of your subscription.\")\n                elif resource == \"apps\" and 0 < apps.limit <= apps.size:\n                    abort(403, \"The number of apps has reached the limit of your subscription.\")\n                elif resource == \"documents\" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:\n                    # The api of file upload is used in the multiple places,\n                    # so we need to check the source of the request from datasets\n                    source = request.args.get(\"source\") or request.form.get(\"source\")\n                    if source == \"datasets\":\n                        abort(403, \"The number of documents has reached the limit of your subscription.\")\n                    else:\n                        return view(*args, **kwargs)\n                elif resource == \"workspace_custom\" and not features.can_replace_logo:\n                    abort(403, \"The workspace custom feature has reached the limit of your subscription.\")\n                elif resource == \"annotation\" and 0 < annotation_quota_limit.limit < annotation_quota_limit.size:\n                    abort(403, \"The annotation quota has reached the limit of your subscription.\")\n                else:\n                    return view(*args, **kwargs)\n\n            return view(*args, **kwargs)\n\n        return decorated\n\n    return interceptor\n\n\ndef cloud_edition_billing_knowledge_limit_check[**P, R](\n    resource: str,\n) -> Callable[[Callable[P, R]], Callable[P, R]]:\n    def interceptor(view: Callable[P, R]):\n        @wraps(view)\n        def decorated(*args: P.args, **kwargs: P.kwargs):","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/controllers/console/wraps.py#L196-L232","documentation":"HTTP 403 from `cloud_edition_billing_resource_check(\"workspace_custom\")`. When `resource == \"workspace_custom\"` and `features.can_replace_logo` is false, the decorator aborts with `The workspace custom feature has reached the limit of your subscription.` Restricts workspace branding/logo replacement to plans that include it.","triggerScenarios":"Calling a workspace-customization route (e.g. logo replacement) on a plan whose `FeatureService.get_features(...).can_replace_logo` flag is false.","commonSituations":"Free/starter plan without branding entitlement; trial of branding expired; features cache stale so `can_replace_logo` reads false after an upgrade.","solutions":["Upgrade to a plan that includes workspace customization.","Gate the branding UI on `features.can_replace_logo` so users cannot trigger the request.","Refresh the features cache after a plan upgrade if the flag still reads false.","Confirm the entitlement is provisioned for the tenant in the billing service."],"exampleFix":"// before\nawait uploadLogo(file)  // 403 on plans without branding\n// after\nconst { can_replace_logo } = await getFeatures()\nif (!can_replace_logo) alert('Workspace branding requires a paid plan.')\nelse await uploadLogo(file)","handlingStrategy":"validation","validationCode":"from services.feature_service import FeatureService\n\n_, tenant_id = current_account_with_tenant()\nfeatures = FeatureService.get_features(tenant_id, exclude_vector_space=True)\n\ndef can_brand() -> bool:\n    return bool(features.can_replace_logo)\n\nif not can_brand():\n    # hide branding UI / show upgrade CTA\n    ...","typeGuard":"def branding_enabled(can_replace_logo: bool | None) -> bool:\n    return bool(can_replace_logo)","tryCatchPattern":"try:\n    resp = client.post(\"/workspace/customize/logo\", ...)\nexcept HTTPError as err:\n    if err.response.status_code == 403 and \"workspace custom\" in err.response.text:\n        # branding entitlement missing — upgrade plan\n        ...\n    raise","preventionTips":["Gate branding UI on features.can_replace_logo.","Upgrade to a plan that includes workspace customization.","Refresh features cache after a plan upgrade.","Confirm the branding entitlement is provisioned in billing."],"tags":["billing","feature-flag","workspace-branding","http-403"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}