dify - ✅(Solved) Fix Backend: gracefully degrade /system-features when enterprise license server is unavailable [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langgenius/dify#35413Fetched 2026-04-20 12:16:05
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
1
Author
Participants
Assignees
Timeline (top)
assigned ×1closed ×1cross-referenced ×1

Discovered while landing #35394 (front-end migration to useSuspenseQuery + loading.tsx / error.tsx). Frontend now soft-falls back to defaultSystemFeatures on 5xx so the dashboard stays usable, but the backend should be the canonical fix because:

  • A partial 200 (community fields real, enterprise fields default) is more informative than a 500 + frontend defaults.
  • It removes the SPOF without coupling frontend availability to enterprise upstream health.

Error Message

api/services/feature_service.py

@classmethod def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel: system_features = SystemFeatureModel() system_features.app_dsl_version = CURRENT_APP_DSL_VERSION cls._fulfill_system_params_from_env(system_features)

if dify_config.ENTERPRISE_ENABLED:
    system_features.branding.enabled = True
    system_features.webapp_auth.enabled = True
    system_features.enable_change_email = False
    system_features.plugin_manager.enabled = True
    try:
        cls._fulfill_params_from_enterprise(system_features, is_authenticated)
    except Exception:
        logger.warning(
            "enterprise enrichment failed; serving system-features with defaults",
            exc_info=True,
        )

if dify_config.MARKETPLACE_ENABLED:
    system_features.enable_marketplace = True

return system_features

Root Cause

Discovered while landing #35394 (front-end migration to useSuspenseQuery + loading.tsx / error.tsx). Frontend now soft-falls back to defaultSystemFeatures on 5xx so the dashboard stays usable, but the backend should be the canonical fix because:

Fix Action

Fixed

PR fix notes

PR #35394: refactor(web): unify app-shell bootstrap on TanStack Query + Next.js route conventions

Description (problem / solution / changelog)

Summary

Closes #35414.

Migrate the systemFeatures / userProfile bootstrap layer from a custom <GlobalPublicStoreProvider> + zustand + <Splash> + useIsLogin stack to a uniform useSuspenseQuery + Next.js loading.tsx / error.tsx architecture. Transport-layer logic (401 / refresh-token / SSE in service/base.ts) is unchanged.

Replaces 5 custom abstractions with 0:

  • useGlobalPublicStore (zustand) → useSuspenseQuery(systemFeaturesQueryOptions())
  • useUserProfile + useIsLogin → shared userProfileQueryOptions() (consumed via useSuspenseQuery inside (commonLayout) and via useQuery + throwOnError: !isLegacyBase401 on signin/oauth pages)
  • <GlobalPublicStoreProvider> blocking render → app/loading.tsx
  • <Splash>(commonLayout)/loading.tsx
  • useSystemFeaturesQuery / useIsSystemFeaturesPending thin wrappers → direct query options

Soft-fallback helper web/service/system-features.ts keeps the dashboard usable when /system-features 5xxs (matches the prior zustand "silent degrade" behavior, but the failure is now logged via console.error for observability).

89 test files migrated to seedSystemFeatures(queryClient, …) + QueryClientProvider via the new web/__tests__/utils/mock-system-features.tsx util.

Screenshots

BeforeAfter
......

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran `make lint && make type-check` (backend) and `cd web && pnpm exec vp staged` (frontend) to appease the lint gods

Changed files

  • eslint-suppressions.json (modified, +1/-6)
  • web/__tests__/app/app-access-control-flow.test.tsx (modified, +7/-29)
  • web/__tests__/app/app-publisher-flow.test.tsx (modified, +7/-29)
  • web/__tests__/apps/app-card-operations-flow.test.tsx (modified, +6/-11)
  • web/__tests__/apps/app-list-browsing-flow.test.tsx (modified, +20/-15)
  • web/__tests__/apps/create-app-flow.test.tsx (modified, +14/-10)
  • web/__tests__/base/chat-flow.test.tsx (modified, +2/-1)
  • web/__tests__/embedded-user-id-store.test.tsx (modified, +4/-36)
  • web/__tests__/explore/explore-app-list-flow.test.tsx (modified, +2/-1)
  • web/__tests__/header/account-dropdown-flow.test.tsx (modified, +8/-26)
  • web/__tests__/plugins/plugin-marketplace-to-install.test.tsx (modified, +1/-10)
  • web/__tests__/plugins/plugin-page-shell-flow.test.tsx (modified, +27/-19)
  • web/__tests__/share/text-generation-index-flow.test.tsx (modified, +5/-9)
  • web/__tests__/tools/provider-list-shell-flow.test.tsx (modified, +14/-11)
  • web/__tests__/tools/tool-browsing-and-filtering.test.tsx (modified, +4/-10)
  • web/__tests__/utils/mock-system-features.tsx (added, +127/-0)
  • web/app/(commonLayout)/error.tsx (added, +33/-0)
  • web/app/(commonLayout)/layout.tsx (modified, +0/-2)
  • web/app/(commonLayout)/loading.tsx (added, +9/-0)
  • web/app/(shareLayout)/webapp-reset-password/layout.tsx (modified, +3/-2)
  • web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx (modified, +3/-2)
  • web/app/(shareLayout)/webapp-signin/layout.tsx (modified, +3/-2)
  • web/app/(shareLayout)/webapp-signin/normalForm.tsx (modified, +3/-2)
  • web/app/(shareLayout)/webapp-signin/page.tsx (modified, +3/-2)
  • web/app/account/(commonLayout)/account-page/index.tsx (modified, +7/-6)
  • web/app/account/(commonLayout)/avatar.tsx (modified, +5/-3)
  • web/app/account/(commonLayout)/header.tsx (modified, +3/-2)
  • web/app/account/oauth/authorize/layout.tsx (modified, +13/-6)
  • web/app/account/oauth/authorize/page.tsx (modified, +11/-5)
  • web/app/activate/page.tsx (modified, +3/-2)
  • web/app/components/__tests__/splash.spec.tsx (removed, +0/-59)
  • web/app/components/app-initializer.tsx (modified, +2/-1)
  • web/app/components/app/app-access-control/__tests__/access-control.spec.tsx (modified, +2/-1)
  • web/app/components/app/app-access-control/__tests__/index.spec.tsx (modified, +13/-14)
  • web/app/components/app/app-access-control/index.tsx (modified, +3/-2)
  • web/app/components/app/app-publisher/__tests__/index.spec.tsx (modified, +6/-11)
  • web/app/components/app/app-publisher/index.tsx (modified, +3/-2)
  • web/app/components/app/create-app-dialog/app-card/__tests__/index.spec.tsx (modified, +2/-1)
  • web/app/components/app/create-app-dialog/app-card/index.tsx (modified, +3/-2)
  • web/app/components/app/overview/__tests__/app-card.spec.tsx (modified, +7/-12)
  • web/app/components/app/overview/app-card.tsx (modified, +3/-2)
  • web/app/components/apps/__tests__/app-card.spec.tsx (modified, +12/-11)
  • web/app/components/apps/__tests__/list.spec.tsx (modified, +8/-11)
  • web/app/components/apps/app-card.tsx (modified, +4/-3)
  • web/app/components/apps/list.tsx (modified, +3/-2)
  • web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx (modified, +2/-1)
  • web/app/components/base/chat/chat-with-history/__tests__/index.spec.tsx (modified, +2/-1)
  • web/app/components/base/chat/chat-with-history/sidebar/__tests__/index.spec.tsx (modified, +9/-49)
  • web/app/components/base/chat/chat-with-history/sidebar/index.tsx (modified, +3/-2)
  • web/app/components/base/chat/embedded-chatbot/__tests__/index.spec.tsx (modified, +12/-30)
  • web/app/components/base/chat/embedded-chatbot/header/__tests__/index.spec.tsx (modified, +12/-90)
  • web/app/components/base/chat/embedded-chatbot/header/index.tsx (modified, +3/-2)
  • web/app/components/base/chat/embedded-chatbot/index.tsx (modified, +3/-2)
  • web/app/components/custom/custom-page/__tests__/index.spec.tsx (modified, +12/-21)
  • web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx (modified, +16/-27)
  • web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts (modified, +3/-2)
  • web/app/components/datasets/create-from-pipeline/list/__tests__/built-in-pipeline-list.spec.tsx (modified, +7/-8)
  • web/app/components/datasets/create-from-pipeline/list/built-in-pipeline-list.tsx (modified, +6/-2)
  • web/app/components/datasets/list/__tests__/index.spec.tsx (modified, +11/-21)
  • web/app/components/datasets/list/index.tsx (modified, +4/-3)
  • web/app/components/devtools/react-scan/loader.tsx (modified, +1/-1)
  • web/app/components/explore/app-card/__tests__/index.spec.tsx (modified, +2/-1)
  • web/app/components/explore/app-card/index.tsx (modified, +3/-2)
  • web/app/components/explore/app-list/__tests__/index.spec.tsx (modified, +22/-11)
  • web/app/components/explore/app-list/index.tsx (modified, +3/-2)
  • web/app/components/explore/try-app/__tests__/index.spec.tsx (modified, +2/-1)
  • web/app/components/explore/try-app/index.tsx (modified, +3/-2)
  • web/app/components/header/__tests__/index.spec.tsx (modified, +25/-28)
  • web/app/components/header/account-about/__tests__/index.spec.tsx (modified, +19/-45)
  • web/app/components/header/account-about/index.tsx (modified, +4/-3)
  • web/app/components/header/account-dropdown/__tests__/index.spec.tsx (modified, +31/-40)
  • web/app/components/header/account-dropdown/index.tsx (modified, +3/-2)
  • web/app/components/header/account-setting/__tests__/index.spec.tsx (modified, +10/-37)
  • web/app/components/header/account-setting/data-source-page-new/__tests__/index.spec.tsx (modified, +21/-50)
  • web/app/components/header/account-setting/data-source-page-new/index.tsx (modified, +6/-2)
  • web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx (modified, +24/-25)
  • web/app/components/header/account-setting/members-page/__tests__/invite-button.spec.tsx (modified, +16/-17)
  • web/app/components/header/account-setting/members-page/index.tsx (modified, +3/-2)
  • web/app/components/header/account-setting/members-page/invite-button.tsx (modified, +3/-2)
  • web/app/components/header/account-setting/members-page/operation/__tests__/transfer-ownership.spec.tsx (modified, +19/-17)
  • web/app/components/header/account-setting/members-page/operation/transfer-ownership.tsx (modified, +3/-2)
  • web/app/components/header/account-setting/model-provider-page/__tests__/index.non-cloud.spec.tsx (modified, +37/-25)
  • web/app/components/header/account-setting/model-provider-page/__tests__/index.spec.tsx (modified, +46/-36)
  • web/app/components/header/account-setting/model-provider-page/index.tsx (modified, +4/-4)
  • web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx (modified, +33/-29)
  • web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx (modified, +4/-3)
  • web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx (modified, +31/-36)
  • web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/quota-panel.spec.tsx (modified, +15/-15)
  • web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/use-credential-panel-state.spec.ts (modified, +24/-23)
  • web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx (modified, +4/-3)
  • web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts (modified, +4/-3)
  • web/app/components/header/index.tsx (modified, +3/-2)
  • web/app/components/header/license-env/__tests__/index.spec.tsx (modified, +8/-23)
  • web/app/components/header/license-env/index.tsx (modified, +3/-2)
  • web/app/components/plugins/install-plugin/hooks/__tests__/use-install-plugin-limit.spec.ts (modified, +2/-14)
  • web/app/components/plugins/install-plugin/hooks/use-install-plugin-limit.tsx (modified, +3/-2)
  • web/app/components/plugins/install-plugin/install-bundle/__tests__/index.spec.tsx (modified, +2/-6)
  • web/app/components/plugins/install-plugin/install-bundle/steps/__tests__/install-multi.spec.tsx (modified, +2/-6)
  • web/app/components/plugins/install-plugin/install-bundle/steps/hooks/__tests__/use-install-multi-state.spec.ts (modified, +2/-5)
  • web/app/components/plugins/install-plugin/install-bundle/steps/hooks/use-install-multi-state.ts (modified, +3/-2)

Code Example

# api/services/feature_service.py
@classmethod
def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel:
    system_features = SystemFeatureModel()
    system_features.app_dsl_version = CURRENT_APP_DSL_VERSION
    cls._fulfill_system_params_from_env(system_features)

    if dify_config.ENTERPRISE_ENABLED:
        system_features.branding.enabled = True
        system_features.webapp_auth.enabled = True
        system_features.enable_change_email = False
        system_features.plugin_manager.enabled = True
        try:
            cls._fulfill_params_from_enterprise(system_features, is_authenticated)
        except Exception:
            logger.warning(
                "enterprise enrichment failed; serving system-features with defaults",
                exc_info=True,
            )

    if dify_config.MARKETPLACE_ENABLED:
        system_features.enable_marketplace = True

    return system_features
RAW_BUFFERClick to expand / collapse

Problem

GET /console/api/system-features is the single source of truth for deployment-level feature flags, branding, and license status. The frontend reads it on every dashboard load (75 useSuspenseQuery callsites after #35394).

In enterprise mode (ENTERPRISE_ENABLED=true), the handler synchronously calls EnterpriseService.get_info() to populate branding, webapp_auth, sso_*, and license.* fields:

https://github.com/langgenius/dify/blob/main/api/services/feature_service.py#L237-L244

EnterpriseService.get_info() issues an HTTP request to the enterprise license server with no try/except, no timeout, no fallback:

https://github.com/langgenius/dify/blob/main/api/services/enterprise/enterprise_service.py#L101-L103

When the enterprise license server is slow, unreachable, or returns 5xx, /system-features returns 500. Until #35394, this turned into a silent zustand fallback to defaultSystemFeatures on the frontend; users saw a degraded-but-usable dashboard.

#35394 introduces a frontend soft-fallback in web/service/system-features.ts that tolerates this failure, but that is a band-aid. The backend treats a transient enterprise-server hiccup as a hard 5xx for all enterprise users at once, including the community-defined fields (enable_email_password_login, enable_marketplace, etc.) that have nothing to do with enterprise data.

This makes the enterprise license server a hidden single point of failure for the enterprise dashboard.

Proposed solution

Wrap the enterprise enrichment in a try/except inside FeatureService.get_system_features, so that:

  1. Community-source fields (env vars, MARKETPLACE_ENABLED, etc.) are always returned successfully.
  2. Enterprise-source fields fall back to model defaults when the enterprise call fails.
  3. The failure is logged with sufficient context for ops to react.

Sketch:

# api/services/feature_service.py
@classmethod
def get_system_features(cls, is_authenticated: bool = False) -> SystemFeatureModel:
    system_features = SystemFeatureModel()
    system_features.app_dsl_version = CURRENT_APP_DSL_VERSION
    cls._fulfill_system_params_from_env(system_features)

    if dify_config.ENTERPRISE_ENABLED:
        system_features.branding.enabled = True
        system_features.webapp_auth.enabled = True
        system_features.enable_change_email = False
        system_features.plugin_manager.enabled = True
        try:
            cls._fulfill_params_from_enterprise(system_features, is_authenticated)
        except Exception:
            logger.warning(
                "enterprise enrichment failed; serving system-features with defaults",
                exc_info=True,
            )

    if dify_config.MARKETPLACE_ENABLED:
        system_features.enable_marketplace = True

    return system_features

Optional follow-ups (not required for this issue):

  • Add a small TTL cache around EnterpriseService.get_info() to absorb repeated upstream blips (similar to get_cached_license_status already does for license).
  • Add a configurable timeout to EnterpriseRequest.send_request defaults.

Acceptance

  • Killing the enterprise license server (or returning 5xx) does not break /system-features; the endpoint returns 200 with community-source fields populated and enterprise-source fields at model defaults.
  • A warning is logged with stack trace on each fallback.
  • No behavior change when the enterprise server is healthy.

Context

Discovered while landing #35394 (front-end migration to useSuspenseQuery + loading.tsx / error.tsx). Frontend now soft-falls back to defaultSystemFeatures on 5xx so the dashboard stays usable, but the backend should be the canonical fix because:

  • A partial 200 (community fields real, enterprise fields default) is more informative than a 500 + frontend defaults.
  • It removes the SPOF without coupling frontend availability to enterprise upstream health.

extent analysis

TL;DR

Wrap the enterprise enrichment in a try/except block to prevent the enterprise license server from being a single point of failure for the enterprise dashboard.

Guidance

  • Identify the FeatureService.get_system_features method and modify it to include a try/except block around the call to EnterpriseService.get_info() to catch any exceptions that may occur.
  • Log a warning with sufficient context when the enterprise call fails, including the exception information, to help ops react to the issue.
  • Ensure that community-source fields are always returned successfully, even if the enterprise call fails, by populating them before the try/except block.
  • Consider adding a small TTL cache around EnterpriseService.get_info() to absorb repeated upstream blips and a configurable timeout to EnterpriseRequest.send_request defaults as follow-up improvements.

Example

The proposed solution sketch provides a clear example of how to implement the try/except block:

try:
    cls._fulfill_params_from_enterprise(system_features, is_authenticated)
except Exception:
    logger.warning(
        "enterprise enrichment failed; serving system-features with defaults",
        exc_info=True,
    )

Notes

The provided solution focuses on preventing the enterprise license server from being a single point of failure, but additional improvements such as caching and timeouts can further enhance the robustness of the system.

Recommendation

Apply the proposed workaround by wrapping the enterprise enrichment in a try/except block to ensure that community-source fields are always returned successfully and enterprise-source fields fall back to model defaults when the enterprise call fails. This approach removes the single point of failure and provides a more informative response to the client.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING