langgenius / langgenius/dify

Rapid disable→enable of a Web App can leave it publicly unavailable (lost update in update_app_site_status)

Open
#39,420 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
156k
Forks
24.6k
Avg merge
22h 9m
Merged PRs (30d)
610

Description

**Self Checks**
- [x] I have read the Contributing Guide and Language Policy.
- [x] This is only for a bug report, not a question.
- [x] I have searched for existing issues, including closed ones.
- [x] I confirm that I am using English to submit this report.
- [x] Please do not modify this template and fill in all the required fields.

**Dify version**

Reproduced against `main` at commit `4da4fa72cd9d011559fad3746547c935941724b2` (Self Hosted / Source). The affected code path is unchanged in current `main`.

**Cloud or Self Hosted**

Self Hosted (Source)

**Steps to reproduce**

1. Create and publish a Web App with public (anonymous) access enabled.
2. Issue two site-status toggles back-to-back, concurrently, without waiting for the first to complete — e.g. `POST /console/api/apps//site-enable` with `{"enable_site": false}` immediately followed by `{"enable_site": true}` (this also happens if an admin rapidly clicks the site-enable toggle in the console overview).
3. After both requests return HTTP 200, read the authoritative app detail.
4. Reload the anonymous Web App URL.

Observed: both requests return HTTP 200, but the final `enable_site` is `false` (the earlier disable), not the later `true` (restore). The public page then returns "App is unavailable".

Root cause — `api/services/app_service.py` (lines 815-832 at HEAD): https://github.com/langgenius/dify/blob/4da4fa72cd9d011559fad3746547c935941724b2/api/services/app_service.py#L815-L832

```python
def update_app_site_status(self, app: App, enable_site: bool, *, session: Session) -> App:
if enable_site == app.enable_site:
return app # stale read -> concurrent enable no-ops
assert current_user is not None
app.enable_site = enable_site
app.updated_by = current_user.id
app.updated_at = naive_utc_now()
session.commit() # last committer wins, no version check
app_was_updated.send(app)
return app
```

Each request runs in its own session with no sequencing or optimistic version/`updated_at` check. When the enable request reads the pre-disable state, the `enable_site == app.enable_site` guard makes it return without writing, while the concurrent disable commits `false` — so the older intent wins.

Effect on public access — `api/controllers/web/passport.py` (lines 76-83): https://github.com/langgenius/dify/blob/4da4fa72cd9d011559fad3746547c935941724b2/api/controllers/web/passport.py#L76-L83

The passport reads `app_model.enable_site` and raises `NotFound()` when it is false, so the stale `false` keeps the public app unreachable until an admin toggles again.

Suggested fix: guard the write with an optimistic concurrency check (e.g. conditional `UPDATE ... WHERE updated_at = ` / a version column), or serialize site-status writes per app, so an older in-flight toggle cannot overwrite a newer one.

**✔️ Expected Behavior**

After two toggles complete, the persisted `enable_site` reflects the most recent request. A restore (`enable_site=true`) issued after a disable should leave the public Web App reachable.

**❌ Actual Behavior**

The final persisted state can reflect the earlier disable rather than the later restore; both requests return HTTP 200, but the public Web App stays unavailable ("App is unavailable") until an admin toggles it again.

Found while running [Ito](https://ito.ai) (AI code review, free for open source) against recently merged PRs — full analysis: https://app.ito.ai/share/b2be8c90-bcb5-4e24-9735-0185a89c8c4d.

Contributor guide

Open the contributing guide

Research direction

Start in api/services/app_service.py at update_app_site_status and trace how concurrent site-status requests read and commit app state. Then read api/controllers/web/passport.py to confirm how enable_site controls anonymous access. Reproduce the paired site-enable requests and consider the issue's expected behavior: the later restore must determine the persisted state and keep the public Web App reachable.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.