Rapid disable→enable of a Web App can leave it publicly unavailable (lost update in update_app_site_status)
- 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
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