fix: keep screenshot recovery after timeout

This commit is contained in:
ik
2026-09-22 19:57:19 +07:00
parent c46b28d1bc
commit d4ccc93894
3 changed files with 7 additions and 7 deletions
+4 -5
View File
@@ -17,10 +17,9 @@ export const POST: APIRoute = async ({ request, redirect, cookies }) => {
cookies.delete("rf4-idempotency-key", {path:"/"});
}
return redirect(response.ok ? "/report?state=screenshot_sent" : `/report?state=screenshot_error&report_id=${encodeURIComponent(reportId)}`, 303);
} catch (err) {
const isTimeout = err instanceof DOMException && err.name === "TimeoutError" ||
err instanceof TypeError && err.message.toLowerCase().includes("fetch") ||
err instanceof Error && err.message.toLowerCase().includes("abort");
return redirect(isTimeout ? `/report?state=timeout&report_id=${encodeURIComponent(reportId)}` : `/report?state=screenshot_error&report_id=${encodeURIComponent(reportId)}`, 303);
} catch {
// The report already exists and the upload can have reached the API before
// the timeout. Keep the recovery form visible; the API makes a retry safe.
return redirect(`/report?state=screenshot_error&report_id=${encodeURIComponent(reportId)}`, 303);
}
};
+2 -1
View File
@@ -22,7 +22,8 @@ const reportId = Astro.url.searchParams.get("report_id");
{state === "timeout" && <div class="notice error" id="form-error" tabindex="-1"><strong>Превышено время ожидания.</strong> Проверьте соединение и попробуйте снова.</div>}
{state === "server_error" && <div class="notice error" id="form-error" tabindex="-1"><strong>Сервер временно недоступен.</strong> Попробуйте позже.</div>}
{state === "screenshot_error" && <div class="notice warning"><strong>Заявка сохранена без скриншота.</strong> Изображение не загрузилось; можно повторить отдельно, не отправляя улов заново.</div>}
{state === "screenshot_error" && reportId && <form class="screenshot-retry" method="post" action="/api/report-screenshot" enctype="multipart/form-data"><input type="hidden" name="report_id" value={reportId} /><label>Повторная загрузка скриншота<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" required /></label><button data-action="inverse" type="submit">Загрузить скриншот</button></form>}
{state === "timeout" && reportId && <div class="notice warning"><strong>Заявка уже сохранена.</strong> Ответ загрузки не успел прийти; повтор безопасен и не создаст новый улов.</div>}
{(state === "screenshot_error" || (state === "timeout" && reportId)) && reportId && <form class="screenshot-retry" method="post" action="/api/report-screenshot" enctype="multipart/form-data"><input type="hidden" name="report_id" value={reportId} /><label>Повторная загрузка скриншота<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" required /></label><button data-action="inverse" type="submit">Загрузить скриншот</button></form>}
{unavailable ? <div class="state" role="alert"><h2>Форма временно недоступна</h2></div> : <form class="report-form" method="post" action="/api/report" enctype="multipart/form-data">
<fieldset><legend>Главное <span>обязательно</span></legend><div class="form-grid"><label>Рыба *<select name="fish_slug" required>{fishes.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Водоём *<select name="waterbody_slug" required>{waterbodies.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Координата X *<input name="x" type="number" min="-10000" max="10000" placeholder="Например, 72" required /></label><label>Координата Y *<input name="y" type="number" min="-10000" max="10000" placeholder="Например, 84" required /></label><label>Вес, граммы *<input name="weight_g" type="number" min="1" max="3000000" placeholder="1250" required /></label><label>Приманка<input name="bait_name" maxlength="200" placeholder="Название в игре" /></label></div><p class="field-help">Координаты — два целых числа с карты. Вес: 1,25 кг = 1250 г.</p></fieldset>
<details class="optional-fields"><summary>Дополнительные сведения <span>необязательно</span></summary><div class="form-grid"><label>Способ ловли<select name="fishing_method"><option value="">Не указан</option><option value="spinning">Спиннинг</option><option value="bottom">Донная</option><option value="float">Поплавочная</option></select></label><label>Проводка<input name="retrieve_method" maxlength="100" /></label><label>Скорость проводки<input name="retrieve_speed" type="number" min="0" max="100" /></label><label>Ник игрока<input name="player_name" maxlength="100" /></label></div><label class="wide">Комментарий<textarea name="comment" maxlength="1000" rows="4"></textarea></label><label class="wide">Скриншот, JPEG/PNG/WebP до 8 МБ<input name="screenshot" type="file" accept="image/jpeg,image/png,image/webp" /></label></details><label class="honeypot" aria-hidden="true">Сайт<input name="website" tabindex="-1" autocomplete="off" /></label><p class="privacy">Ник и скриншот необязательны. Из изображения удаляются EXIF и прочие метаданные.</p><label class="consent"><input name="consent" type="checkbox" required /> <span>Я отправляю собственное наблюдение и принимаю <a href="/rules" target="_blank">правила</a> и <a href="/privacy" target="_blank">политику конфиденциальности</a>.</span></label><button data-action="primary" type="submit">Отправить на проверку</button>