WeblateOrg / WeblateOrg/weblate
Units created via the API immediately after component creation are silently deleted
- Dominant language
- Python
- Stars
- 6.1k
- Forks
- 1.4k
- Avg merge
- 9h 53m
- Merged PRs (30d)
- 395
Description
### Describe the issue
Creating a translation unit through `POST /api/translations/{project}/{component}/{language}/units/` immediately after creating its parent component succeeds (`HTTP 200`, full unit body returned, confirmed by a follow-up `GET`), but the unit is then **silently deleted a few hundred milliseconds later** with no error surfaced anywhere.
The parent component's own post-creation initialization task chain (`component_after_save` → `perform_load`) reconciles the database against the on-disk files shortly after creation. Because the newly-created component's files are still empty at that point, and the unit created via the API is a pending row that was never serialized into a file, the reconciliation pass treats it as "stale" and deletes it. This is **not** a lost-update race between two writers — `add_unit()` does hold the component lock while writing — it's a race between that write and the component's own later, separately-locked initialization pass, which runs its reconciliation with `preserve_pending_units=False` by default.
No explicit `repository`/`commit` call is needed to trigger the deletion — it happens automatically as part of component setup, typically within ~100–200ms of component creation. Waiting roughly 15 seconds before creating the unit avoids the issue entirely.
### I already tried
- [x] I've read and searched the documentation.
- [x] I've searched for similar filed issues in this repository.
(Closest related issue I found: #13623, "Redesign component level locking," whose own checklist names "creating new strings (add, parsing files)" as a scenario not yet covered by that locking work.)
### Steps to reproduce the behavior
1. Create a project: `POST /api/projects/` with a `slug`.
2. Immediately create a component under it, e.g. `file_format: json`, `repo: "local:"`, `vcs: "local"`, `manage_units: true`.
3. Immediately (no delay) `POST` a unit: `POST /api/translations/{project}/{component}/{source_language}/units/` with a `key` and `value`.
4. Observe: `HTTP 200`, full unit body returned. A follow-up `GET` on the same units endpoint shows `count: 1` with the new unit present.
5. Poll the same units list every 1–2 seconds for 10–20 seconds, without calling any other endpoint.
6. Observe: within roughly 100ms–a few seconds, the unit disappears — `count: 0` — with no error response, webhook, or log entry visible to the API caller at any point.
Additional scope notes from testing:
- Inserting a ~15 second delay before step 3 makes the unit persist correctly.
- Bulk upload (`POST .../file/`) done immediately after component creation is **not** affected — units created that way land reliably even with zero delay.
- The same loss reproduces against a real git-backed component (not just `vcs: "local"`), though the vulnerable window is narrower and less deterministic there, since the clone/checkout step itself can consume most of the window before an external client's next request lands.
### Expected behavior
Either:
- Reject or defer unit creation while the component is still initializing (e.g. `409 Conflict` or `503` with `Retry-After`), or
- Expose a readiness indicator on the component resource so a client can poll before writing, or
- Have the reconciliation pass preserve pending units created during initialization instead of discarding them.
Silently returning `200` for a write that is then discarded with no trace is the part that needs fixing regardless of which direction is chosen.
### Screenshots
N/A — this is a pure API/backend timing issue with no UI surface. See "Additional context" for a server-log excerpt showing the deletion.
### Exception traceback
N/A — no exception is raised; the unit is deleted cleanly by normal reconciliation logic, which is exactly the problem (nothing signals that anything went wrong).
### How do you run Weblate?
Docker container
### Weblate version
2026.7.1 (also confirmed against the current `latest` Docker tag, which at the time of testing resolved to the same image)
### Weblate deploy checks
N/A — reproduced on a minimal, disposable single-node instance for isolation; not a production deployment.
### Additional context
Server log for the exact run above (component `bugdemo/c`, `vcs: local`), timestamps included — the unit was created at `07:32:25,875` (per the API response's own `timestamp` field, `2026-08-02T07:32:25.878448Z`) and deleted 130ms later, entirely on its own, before the first poll even landed:
```
[2026-08-02 07:32:25,804: INFO] bugdemo/c: Component created (admin)
[2026-08-02 07:32:25,844: INFO] Task weblate.trans.tasks.component_after_save[...] received
200, "pending": true, timestamp 07:32:25.878448Z>
[2026-08-02 07:32:25,976: INFO] bugdemo/c: checking l10n/en.json (en) [1/1]
[2026-08-02 07:32:25,995: INFO] bugdemo/c/en: processing l10n/en.json, new file, 0 strings
[2026-08-02 07:32:26,005: INFO] bugdemo/c/en: deleting 1 stale strings
[2026-08-02 07:32:26,013: INFO] bugdemo/c/en: updating completed
```
Same result against a real git-backed component (`vcs: "git"`, cloned from a live repo rather than `local:`) — narrower window, but reproduced on the first attempt:
```
[2026-08-02 07:33:52,446: INFO] bugdemo/cgit1: Component created (admin)
200, timestamp 07:33:52.482971Z>
[2026-08-02 07:33:52,568: INFO] bugdemo/cgit1: checking l10n/en.json (en) [1/1]
[2026-08-02 07:33:52,573: INFO] bugdemo/cgit1/en: processing l10n/en.json, new file, 0 strings
[2026-08-02 07:33:52,582: INFO] bugdemo/cgit1/en: deleting 1 stale strings
```
Both runs used `json` file format; I've also seen the same behavior with `properties`.
Related: #13623 covers component-level locking generally and its checklist names this exact "creating new strings" gap as unresolved work; none of the PRs that came out of that effort (#15248, #15250, #15277, #18747, #18759, #19671) add a guard between API-driven unit creation and the initialization reconciliation pass. Discussion #15594 (pending translations lost during repository sync) is a related but distinct report worth cross-referencing — different alleged trigger, same general "destructive reconciliation of pending work" pattern.
Contributor guide
Research direction
Start by reproducing the immediate POST /api/translations/{project}/{component}/{language}/units/ sequence and trace the component_after_save → perform_load task chain. Inspect add_unit() and the preserve_pending_units=False reconciliation path; done means a unit created during initialization is not silently deleted, with regression coverage for the reported timing and initialization behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100