(medium) download.ts/triggerDownload: синхронный revokeObjectURL в том же тике после a.click() — гонка, срывающая скачивание в Firefox/Safari #1488

Closed
opened 2026-06-15 05:58:35 +00:00 by bot-backend · 1 comment
Collaborator

Severity: medium · Категория: frontend · Файл: frontend/src/lib/download.ts:18-20

download.ts:12-21. После a.click() (стр.18) в том же синхронном тике вызываются document.body.removeChild(a) и URL.revokeObjectURL(url) (стр.19-20). a.click() лишь ставит скачивание в очередь — браузеру нужно прочитать blob по object URL уже после возврата из обработчика. Синхронный revoke освобождает blob до того, как download-конвейер успевает его забрать. Это известная гонка: Chromium обычно терпит синхронный revoke, а Firefox и исторически Safari периодически отменяют загрузку без видимой ошибки. Импакт: пользователь нажимает экспорт (PDF-снапшот участка, forecast .md/.json, GeoJSON/CSV концепции) и не получает файл, плавающе. Как чинить: отложить освобождение — setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0) (на практике используют задержку ~250мс–1с) либо requestAnimationFrame, чтобы blob дожил до старта скачивания.

Почему баг: a.click() лишь СТАВИТ В ОЧЕРЕДЬ навигацию/скачивание — браузеру нужно прочитать blob по object URL уже после возврата из обработчика. Синхронный revokeObjectURL в том же тике освобождает blob до того, как download-конвейер успевает его забрать. Это известная гонка: в Firefox (и исторически Safari) загрузка периодически срывается/отменяется, файл не скачивается. Chromium обычно терпит синхронный revoke, поэтому баг плавающий, а не стопроцентный — отсюда medium confidence. Импакт: клиент-девелопер нажимает «Снапшот PDF» / экспорт прогноза и не получает файл без видимой ошибки.

Как чинить: Отложить освобождение: a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0) (или revoke на следующий tick / по событию). Не вызывать revokeObjectURL синхронно сразу после click().


deep-audit «армия ботов 2» · verify+harden, confidence 0.8 · unit U108

**Severity:** medium · **Категория:** frontend · **Файл:** `frontend/src/lib/download.ts:18-20` download.ts:12-21. После `a.click()` (стр.18) в том же синхронном тике вызываются `document.body.removeChild(a)` и `URL.revokeObjectURL(url)` (стр.19-20). `a.click()` лишь ставит скачивание в очередь — браузеру нужно прочитать blob по object URL уже после возврата из обработчика. Синхронный revoke освобождает blob до того, как download-конвейер успевает его забрать. Это известная гонка: Chromium обычно терпит синхронный revoke, а Firefox и исторически Safari периодически отменяют загрузку без видимой ошибки. Импакт: пользователь нажимает экспорт (PDF-снапшот участка, forecast .md/.json, GeoJSON/CSV концепции) и не получает файл, плавающе. Как чинить: отложить освобождение — `setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0)` (на практике используют задержку ~250мс–1с) либо `requestAnimationFrame`, чтобы blob дожил до старта скачивания. **Почему баг:** `a.click()` лишь СТАВИТ В ОЧЕРЕДЬ навигацию/скачивание — браузеру нужно прочитать blob по object URL уже после возврата из обработчика. Синхронный `revokeObjectURL` в том же тике освобождает blob до того, как download-конвейер успевает его забрать. Это известная гонка: в Firefox (и исторически Safari) загрузка периодически срывается/отменяется, файл не скачивается. Chromium обычно терпит синхронный revoke, поэтому баг плавающий, а не стопроцентный — отсюда medium confidence. Импакт: клиент-девелопер нажимает «Снапшот PDF» / экспорт прогноза и не получает файл без видимой ошибки. **Как чинить:** Отложить освобождение: `a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0)` (или revoke на следующий tick / по событию). Не вызывать revokeObjectURL синхронно сразу после click(). --- <sub>deep-audit «армия ботов 2» · verify+harden, confidence 0.8 · unit U108</sub>
bot-backend added the
week ревью 1
label 2026-06-15 05:58:35 +00:00
bot-backend added the
scope/frontend
label 2026-06-15 11:02:12 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: frontend/src/lib/download.ts:12-21triggerDownload: a.click() (line 18), затем СИНХРОННО в том же тике document.body.removeChild(a) (line 19) и URL.revokeObjectURL(url) (line 20).

Корень: a.click() лишь ставит скачивание в очередь — браузер читает blob по object URL уже после возврата из обработчика. Синхронный revokeObjectURL освобождает blob до того, как download-конвейер его забирает. Известная гонка: Chromium обычно терпит, Firefox (и исторически Safari) периодически отменяют загрузку без ошибки → файл не скачивается, плавающе. Подтверждены 3 call-site: ExportButtons.tsx:111 (PDF), :130 (CSV), ForecastExportButtons.tsx:108 (.md/.json) — все идут через эту функцию.

Что править:

  • Отложить освобождение на следующий tick:
    a.click();
    setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
    
    (на практике используют ~250мс–1с; requestAnimationFrame тоже вариант). НЕ revoke синхронно сразу после click().

Что посмотреть:

  • Код: lib/download.ts:12-21; call-sites ExportButtons.tsx:111,130, ForecastExportButtons.tsx:108 — менять не нужно, фикс локализован в helper.
  • Vault: релевантных заметок не найдено.
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** `frontend/src/lib/download.ts:12-21` — `triggerDownload`: `a.click()` (line 18), затем СИНХРОННО в том же тике `document.body.removeChild(a)` (line 19) и `URL.revokeObjectURL(url)` (line 20). **Корень:** `a.click()` лишь ставит скачивание в очередь — браузер читает blob по object URL уже после возврата из обработчика. Синхронный `revokeObjectURL` освобождает blob до того, как download-конвейер его забирает. Известная гонка: Chromium обычно терпит, Firefox (и исторически Safari) периодически отменяют загрузку без ошибки → файл не скачивается, плавающе. Подтверждены 3 call-site: `ExportButtons.tsx:111` (PDF), `:130` (CSV), `ForecastExportButtons.tsx:108` (.md/.json) — все идут через эту функцию. **Что править:** - Отложить освобождение на следующий tick: ```ts a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 0); ``` (на практике используют ~250мс–1с; `requestAnimationFrame` тоже вариант). НЕ revoke синхронно сразу после click(). **Что посмотреть:** - Код: `lib/download.ts:12-21`; call-sites `ExportButtons.tsx:111,130`, `ForecastExportButtons.tsx:108` — менять не нужно, фикс локализован в helper. - Vault: релевантных заметок не найдено.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1488
No description provided.