diff --git a/History/upload-analysis-v40.md b/History/upload-analysis-v40.md new file mode 100644 index 0000000..b911db4 --- /dev/null +++ b/History/upload-analysis-v40.md @@ -0,0 +1,102 @@ +# Запрос на анализ: XHR onerror («Сеть») при POST /upload с телом > 100KB + +**Дата:** 2026-06-17 +**Версия:** v1.40 +**ТОЛЬКО АНАЛИЗ. КОД НЕ ПРАВИТЬ.** + +--- + +## Симптом + +Браузер отправляет XHR POST на `/upload` с JSON-телом ~200KB (base64 файла). Прогресс 100% (данные ушли). Затем `xhr.onerror` → «Сеть». Сервер иногда успевает обработать (логи подтверждают), иногда нет. + +Прямой curl с такими же данными — иногда HTTP 200, иногда ReadTimeout. + +## Что ПРОБОВАЛИ (40 версий) + +| v | Подход | Результат | +|---|---|---| +| 1.0-1.13 | FormData multipart POST | ✗ Сеть | +| 1.14-1.20 | Чанки 50KB, пул БД, keepalive | ✗ | +| 1.21-1.25 | JSON/base64, TEXT вместо BYTEA | частично ✓ | +| 1.26 | `original_bytes DROP NOT NULL` + `original_b64 TEXT` | ✓ РАБОТАЛО (!) | +| 1.27-1.36 | camelot, tabula, libreoffice для PDF | регресс загрузки | +| 1.37 | Логи в памяти | ✗ | +| 1.38-1.39 | Redis очередь (RPUSH → 200 сразу) | ✗ | +| 1.40 | Сырой regex вместо get_json() | ✗ | + +## Текущий код (самое быстрое что смогли) + +```python +# app.py — НЕТ JSON-парсинга, НЕТ base64.decode(), НЕТ DB INSERT +def _upload_json_impl(self): + raw = request.get_data(as_text=True) + import re + m = re.search(r'"data"\s*:\s*"([^"]*)"', raw) + b64 = m.group(1) + m = re.search(r'"filename"\s*:\s*"([^"]*)"', raw) + filename = m.group(1) if m else "" + m = re.search(r'"cid"\s*:\s*"([^"]*)"', raw) + cid = m.group(1) if m else None + + # Контракт — синхронно (быстро) + if not cid: + conn = db.connect() + cur = conn.cursor() + cur.execute("INSERT INTO contracts ... RETURNING id") + cid = cur.fetchone()[0] + conn.commit() + db.put_conn(conn) + + # Файл — в Redis (мгновенно, без DB) + redis_client.push({"filename": filename, "b64": b64, "mime": mime, "cid": str(cid)}) + return jsonify({"contract_id": str(cid)}) +``` + +```python +# db.py — пул с keepalive +ThreadedConnectionPool(minconn=2, maxconn=10, keepalives=1, connect_timeout=10) +``` + +```javascript +// upload.html — XHR без ретраев +var xhr = new XMLHttpRequest(); +xhr.open('POST', '/upload'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.timeout = 30000; +xhr.onerror = function() { reject(new Error('Сеть')); }; +xhr.send(JSON.stringify({filename: file.name, data: b64, cid: cid || null})); +``` + +## Инфраструктура + +``` +Браузер → ddos-guard → nginx/Ingress → gunicorn → Flask + ↓ + Redis (RPUSH) → воркер (BRPOP → DB) +``` + +- Платформа: pythonk8s.services.ngcloud.ru (managed) +- Redis: redisk8s....svc.cluster.local +- PostgreSQL: postgresqlk8s-master...svc.cluster.local +- Пул БД: 2-10 соединений, keepalive + +## Тесты curl + +``` +100KB #1: HTTP:000 +100KB #2: HTTP:000 +100KB #3: HTTP:200 +100KB #4: HTTP:000 +100KB #5: HTTP:000 + +10KB: всегда HTTP 200 +``` + +## Вопросы + +1. Почему `request.get_data()` для 200KB тела иногда убивает соединение ещё до ответа? Это на уровне gunicorn/Flask/Werkzeug? +2. Может ли nginx/Ingress иметь `client_max_body_size` или `proxy_request_buffering` который режет тело? +3. Почему v1.26 работал, а v1.40 с тем же `/upload` (без изменений в логике) — нет? +4. Может ли проблема быть в HTTP/2 vs HTTP/1.1? curl с `--http2` иногда помогает на этом хостинге. +5. Redis внутри кластера — может ли `redis.Redis()` блокироваться на DNS-резолве `.svc.cluster.local`?