v1.0.177: History — объединение history+History, 5 подпапок

This commit is contained in:
“Naeel”
2026-06-25 07:53:56 +04:00
parent 14cca9f8cf
commit c40d8eb5a8
54 changed files with 0 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
# Запрос на анализ: чанковая загрузка падает с «Сеть»
**Дата:** 2026-06-17
**Версия:** v1.17
**СТРОГО: ТОЛЬКО АНАЛИЗ. КОД НЕ ПРАВИТЬ. Ответ — в этот же файл.**
---
## Симптом
Загрузка PDF 153 KB (3 чанка по 50 KB) через `POST /chunk`:
- Прогресс быстро доходит до 100%
- Затем ~10 секунд висит «Загрузка...»
- Итог: `✗ Сеть` (XHR onerror — обрыв соединения, нет HTTP-ответа)
Пробовали:
- v1.15: чанки в памяти Python `_chunk_store = {}` → «Сеть»
- v1.17: чанки в PostgreSQL (таблица `chunks`) → ТО ЖЕ САМОЕ
Значит дело НЕ в worker'ах/shared memory — с БД чанки общие для всех процессов.
## Архитектура
- **Платформа:** pythonk8s.services.ngcloud.ru (managed Flask-хостинг, деплой = git push)
- **Ingress:** nginx перед приложением, таймаут ~30 сек (известно по предыдущему опыту)
- **Приложение:** Flask (app.run), Python 3.12
- **БД:** PostgreSQL 17.6, внутренний кластер
## Клиентский код (upload.html)
```javascript
async function uploadFileChunked(file, cid, onProgress) {
var CHUNK_SIZE = 50 * 1024; // 50 KB
var totalChunks = Math.ceil(file.size / CHUNK_SIZE);
var uploadId = file.name + '_' + Date.now() + '_' + Math.random();
for (var i = 0; i < totalChunks; i++) {
var chunk = file.slice(i * CHUNK_SIZE, Math.min((i+1) * CHUNK_SIZE, file.size));
var resp = await new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/chunk');
xhr.timeout = 60000;
xhr.onload = function() {
if (xhr.status === 200) {
var r = JSON.parse(xhr.responseText);
if (r.error) reject(new Error(r.error));
else resolve(r);
} else reject(new Error('HTTP ' + xhr.status));
};
xhr.onerror = () => reject(new Error('Сеть'));
xhr.ontimeout = () => reject(new Error('Таймаут'));
var fd = new FormData();
fd.append('chunk', chunk);
fd.append('upload_id', uploadId);
fd.append('filename', file.name);
fd.append('chunk_index', i);
fd.append('total_chunks', totalChunks);
if (cid) fd.append('cid', cid);
xhr.send(fd);
});
// resp = {contract_id: "..."} только на последнем чанке
// или {chunk: N, received: N, total: N} на промежуточных
}
}
```
Файл 153 KB → 3 чанка. Прогресс 33% → 67% → 100% (значит все 3 XHR завершились). Затем 10 сек паузы → «Сеть».
## Серверный код (app.py)
```python
def _chunk_upload(self):
upload_id = request.form.get("upload_id")
chunk_index = int(request.form.get("chunk_index", 0))
total_chunks = int(request.form.get("total_chunks", 1))
chunk_data = request.files.get("chunk").read()
# Сохранить в БД
db.execute(
"INSERT INTO chunks (...) VALUES (...) ON CONFLICT DO NOTHING",
(upload_id, chunk_index, chunk_data, ...)
)
# Проверить: все чанки получены?
received = db.query("SELECT COUNT(*) FROM chunks WHERE upload_id=%s", ...)
if received == total_chunks:
# Собрать из БД
rows = db.query("SELECT chunk_data FROM chunks WHERE upload_id=%s ORDER BY chunk_index", ...)
file_bytes = b"".join(...)
# Сохранить как документ
_save_file_to_db(filename, file_bytes, mime, contract_id)
# Очистить чанки
db.execute("DELETE FROM chunks WHERE upload_id=%s", ...)
return jsonify({"contract_id": str(contract_id)})
return jsonify({"chunk": chunk_index, "received": received, "total": total_chunks})
```
## Вопросы для анализа
1. **Почему «Сеть» (XHR onerror), а не HTTP-ошибка?** Сервер не возвращает ответ — соединение обрывается на уровне TCP. Это не таймаут (был бы `ontimeout`) и не HTTP-ошибка (был бы `onload`).
2. **Почему прогресс 100% но потом 10 сек паузы?** Все 3 чанка отправлены и получили ответ? Или третий чанк висит 10 сек и потом обрыв?
3. **Может ли `ON CONFLICT DO NOTHING` + `SELECT COUNT(*)` дать гонку?** Два worker'а одновременно пишут чанки — COUNT может не увидеть только что вставленный?
4. **Может ли сборка (`b"".join` + `_save_file_to_db`) быть медленной и вызывать таймаут Ingress?** 153 KB — вроде быстро, но если БД тормозит...
5. **Может ли `client_max_body_size` на nginx резать чанки?** 50 KB — вроде мало.
6. **Есть ли ограничение на количество запросов от одного клиента?** Rate limiting?
7. **Что видят логи сервера?** Есть ли способ посмотреть?
## Что нужно
**ТОЛЬКО АНАЛИЗ. КОД НЕ ТРОГАТЬ.**
1. Определить наиболее вероятную причину обрыва
2. Предложить способ диагностики (как узнать точно)
3. Предложить решение (без правки кода)
@@ -0,0 +1,197 @@
# Запрос на анализ: случайные обрывы соединения при HTTP POST
**Дата:** 2026-06-17
**Проект:** Contracts App (Flask + PostgreSQL на managed-хостинге)
**Версия:** v1.21
---
## Симптом
Любой HTTP POST на managed-хостинг pythonk8s.services.ngcloud.ru **случайно** обрывается:
```
100KB #1: HTTP:000
100KB #2: HTTP:000
100KB #3: HTTP:200
100KB #4: HTTP:000
100KB #5: HTTP:000
```
- HTTP 000 — curl не может установить TCP-соединение (Connection reset? Connection refused?)
- Не зависит от: размера (17KB падает, 35KB работает), типа файла, кодировки
- FormData и JSON — одинаково
- Health check (GET /health, без БД) — всегда 200 (5/5)
- Прямой доступ к БД через /test (JSON) — всегда работает
## Инфраструктура
```
Клиент → ddos-guard → nginx/Ingress → gunicorn → Flask (1 worker?)
↓
PostgreSQL 17.6
```
- Managed-хостинг: pythonk8s.services.ngcloud.ru
- Деплой: git push master → автосборка
- БД: внутренний кластер postgresqlk8s-master...svc.cluster.local
- Код: Flask (app.run, host=0.0.0.0:5000)
## Что пробовали
1. **FormData/multipart** — POST / с файлом → ✗
2. **Чанки 50KB** (FormData) → только первый чанк доходит
3. **Чанки в PostgreSQL** (таблица chunks) → только первый чанк
4. **Пул соединений psycopg2** (ThreadedConnectionPool min=1 max=3) → ✗
5. **JSON/base64** (POST /upload, Content-Type: application/json) → ✗
6. **Прямой INSERT в БД** через /test endpoint (JSON) → ✓ всегда
## Код
### Сервер: простое сохранение файла в БД
```python
# app.py
def _upload_json(self):
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Нет JSON"}), 400
filename = data.get("filename", "")
b64 = data.get("data", "")
if not filename or not b64:
return jsonify({"error": "Нет filename или data"}), 400
import base64 as b64mod
file_bytes = b64mod.b64decode(b64)
mime = mimeutil.guess_mime(filename) or "application/octet-stream"
cid = data.get("cid")
conn = None
if cid:
contract_id = cid
else:
conn, err = db.connect()
if err:
return jsonify({"error": f"БД: {err}"}), 500
cur = conn.cursor()
cur.execute(
"INSERT INTO contracts (number, client) VALUES (%s, %s) RETURNING id",
("б/н " + __import__("datetime").datetime.now().strftime("%Y%m%d-%H%M"), ""),
)
contract_id = cur.fetchone()[0]
conn.commit()
doc_id = _save_file_to_db(filename, file_bytes, mime, str(contract_id), conn=conn)
if conn:
cur.close()
db.put_conn(conn)
return jsonify({"contract_id": str(contract_id), "doc_id": str(doc_id) if doc_id else None})
```
### Пул соединений (db.py)
```python
import os
from psycopg2 import pool as _pgpool
_pool = None
def _get_pool():
global _pool
if _pool is None:
_pool = _pgpool.ThreadedConnectionPool(
minconn=1,
maxconn=3,
host=os.getenv("DB_HOST"),
port=os.getenv("DB_PORT", 5432),
dbname=os.getenv("DB_NAME"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASS"),
sslmode=os.getenv("DB_SSLMODE", "disable"),
)
return _pool
def connect():
try:
return _get_pool().getconn(), None
except Exception as e:
return None, str(e)
def put_conn(conn):
try:
_get_pool().putconn(conn)
except Exception:
pass
def execute(sql_text, params=None):
conn, err = connect()
if err:
return None, f"connect: {err}"
try:
cur = conn.cursor()
cur.execute(sql_text, params)
conn.commit()
rowcount = cur.rowcount
cur.close()
return rowcount, None
except Exception as e:
try: conn.rollback()
except: pass
return None, str(e)
finally:
put_conn(conn)
```
### Клиент (JavaScript)
```javascript
function readFileAsBase64(file, onProgress) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() {
var b64 = reader.result.split(',')[1];
resolve(b64);
};
reader.onerror = function() { reject(new Error('Read error')); };
reader.readAsDataURL(file);
});
}
function uploadFileJSON(file, cid, onProgress) {
return new Promise(function(resolve, reject) {
readFileAsBase64(file, onProgress).then(function(b64) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = 120000;
xhr.onload = function() {
if (xhr.status === 200) {
try {
var resp = JSON.parse(xhr.responseText);
if (resp.error) { reject(new Error(resp.error)); return; }
resolve(resp);
} catch(e) { reject(new Error('Bad JSON')); }
} else {
reject(new Error('HTTP ' + xhr.status));
}
};
xhr.onerror = function() { reject(new Error('Сеть')); };
xhr.ontimeout = function() { reject(new Error('Таймаут')); };
xhr.send(JSON.stringify({
filename: file.name,
data: b64,
cid: cid || null
}));
}).catch(reject);
});
}
```
## Вопросы
1. Почему health check (GET /health, просто return "OK") всегда работает, а POST с любым телом — случайно обрывается?
2. Может ли проблема быть в ddos-guard, который считает частые POST за флуд?
3. Может ли gunicorn с 1 worker'ом не справляться с последовательными запросами (keepalive, таймауты)?
4. Почему curl с локалхоста показывает HTTP 000 (нет TCP-соединения), а не HTTP 502/504?
5. Может ли managed PostgreSQL обрывать соединения пула, вызывая каскадный отказ?
+89
View File
@@ -0,0 +1,89 @@
# План: Порядок файлов вместо Базового radio
**v1.0.106+**
## Суть
Убрать столбец «Базовый» с radio-кнопкой. Вместо этого:
- Первый файл в списке = основной договор (исходная спецификация)
- Порядок файлов определяет последовательность обработки в сравнении
- Перемещение стрелками ↑↓
- Первая строка визуально выделена
## UI (index.cfm)
### Столбцы
`Имя | Изменён | Размер | Парсинг | ↕ | Текст | ✕`
### Строка
```html
<tr class="first-row"> <!-- если первая -->
<td>имя</td>
<td>дата</td>
<td>размер</td>
<td>статус</td>
<td>
<button onclick="moveUp(i)" [disabled если первая]>↑</button>
<button onclick="moveDown(i)" [disabled если последняя]>↓</button>
</td>
<td><button onclick="showText(i)">Текст</button></td>
<td><button onclick="removeFile(i)">✕</button></td>
</tr>
```
### CSS
```css
.first-row td { background: rgba(37,99,235,.05); font-weight: 600; }
.arrow-btn { cursor: pointer; color: var(--muted); background: none; border: none; padding: 2px 4px; font-size: 12px; }
.arrow-btn:hover { color: var(--brand-primary); }
.arrow-btn:disabled { opacity: .3; cursor: default; }
```
### JS
```js
function moveUp(i) {
if (i <= 0) return;
var tmp = fileQueue[i]; fileQueue[i] = fileQueue[i-1]; fileQueue[i-1] = tmp;
renderTable();
}
function moveDown(i) {
if (i >= fileQueue.length - 1) return;
var tmp = fileQueue[i]; fileQueue[i] = fileQueue[i+1]; fileQueue[i+1] = tmp;
renderTable();
}
```
## Бэкенд (convert_server.py)
### Передача порядка
EventSource URL: `/process-v2?contract_id=X&order=sid1,sid2,sid3`
### Обработка
В `_handle_process_v2`:
- Если есть `order` — обрабатывать supplements в этом порядке
- Если нет — `ORDER BY created_at` (как сейчас)
```python
order = params.get("order", [None])[0]
if order:
ordered_ids = order.split(",")
# Переупорядочить supps по ordered_ids
supps.sort(key=lambda s: ordered_ids.index(s["id"]) if s["id"] in ordered_ids else 999)
```
### UI — построение order
```js
var order = fileQueue.map(function(f) { return f.supp_id; }).filter(Boolean).join(',');
var es = new EventSource('/process-v2?contract_id=' + contractId + '&order=' + order);
```
## Что убрать
- `<th>Базовый</th>` из заголовка
- Radio-кнопки из строк
- `setInitial()` JS-функцию
- `action=set_initial` в api.cfm (опционально, не мешает)
## Приоритет
Сделать когда вернём все security-фиксы и деплой заработает.
+60
View File
@@ -0,0 +1,60 @@
# Ответ: HTTP/2 из Python к aillm.ru
_Дата: 2026-06-14_
---
## Проблема
`api.aillm.ru` защищен ddos-guard, который требует наличия HTTP/2 в TLS-хендшейке от облачных IP. Библиотека `requests` работает только по HTTP/1.1, что приводит к ложным ошибкам 401/403.
## Решение
Использование библиотеки **`httpx`** с установленной поддержкой HTTP/2 через чистый Python-пакет **`h2`**.
### 1. Подготовка зависимостей (requirements.txt)
Так как облачная платформа не всегда корректно обрабатывает синтаксис `httpx[http2]`, необходимо прописать зависимости **явно отдельными строками**. Это гарантирует установку `h2` — pure Python реализации протокола, которая не требует системных библиотек или `apt`.
```text
# requirements.txt
httpx
h2
```
### 2. Код клиента (llm_client.py)
Для работы по HTTP/2 в `httpx` нужно явно передать параметр `http2=True` при создании клиента.
```python
import os
import httpx
def ask_llm(prompt: str):
api_url = "https://api.aillm.ru/v1/chat/completions"
api_key = os.getenv("LLM_API_KEY", "sk-ucI5YvOticoOQ9Kuj5K9mQ")
payload = {
"model": "gpt-oss-120b",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 8000,
"temperature": 0.1
}
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
# ВАЖНО: http2=True включает поддержку нужного протокола
with httpx.Client(http2=True, timeout=120) as client:
resp = client.post(api_url, json=payload, headers=headers)
resp.raise_for_status()
return resp.json()
```
## Почему это работает в slim-контейнере
1. **`httpx`** — современный клиент, поддерживающий HTTP/2.
2. **`h2`** — это замена системным библиотекам `nghttp2`. Она написана на чистом Python, поэтому корректно устанавливается через `pip` даже в максимально «обрезанных» образах (\*-slim, alpine) без компиляторов и `apt`.
3. **HTTP/2 Fingerprint** — ddos-guard видит поддержку HTTP/2 в TLS Client Hello, что является для него признаком «легального» клиента (браузера или современного SDK), а не простого скрипта на `requests` или старого бота.
## Резюме для реализации
- Используйте `httpx.Client(http2=True)`.
- Убедитесь, что `h2` есть в `requirements.txt` отдельной строкой.
- Не используйте `httpx[http2]` в конфигах, если платформа капризная.
+45
View File
@@ -0,0 +1,45 @@
# Вопрос: HTTP/2 из Python к aillm.ru
## Контекст
Flask-приложение в контейнере `python:3.12-slim`, деплоится через облачную платформу.
Нужно отправлять промпты к LLM API: `https://api.aillm.ru/v1/chat/completions`
(ключ: `sk-ucI5YvOticoOQ9Kuj5K9mQ`, модель: `gpt-oss-120b`).
## Проблема
`api.aillm.ru` за ddos-guard, который **требует HTTP/2 от облачных IP**.
С локальной машины работает любой HTTP-клиент (requests, curl без --http2),
но из облачного контейнера — только HTTP/2.
Что пробовали, что НЕ сработало:
- `requests` — HTTP/1.1, ddos-guard возвращает ложный 401
- `httpx[http2]` в requirements.txt — платформа не парсит `[http2]`, h2 не ставится
- `curl` через subprocess — curl не установлен в slim-контейнере, apt недоступен
- свой HTTP/2 клиент на h2+socket — слишком сложно, заказчик против
## Вопрос
**Как в Python из slim-контейнера сделать HTTP/2 POST-запрос к aillm.ru?**
Требования:
1. Никакого apt/curl — только pip
2. Просто, без велосипедов на сокетах
3. Работает в python:3.12-slim
4. Ключ передаётся как `Authorization: Bearer sk-ucI5YvOticoOQ9Kuj5K9mQ`
5. Тело: JSON с model, messages, max_tokens, temperature
Доступные модели на aillm.ru: `gpt-oss-120b`, `qwen3.6-27b-fp8`, `qwen3-6-27b-fp8-opt`, `whisper-large-v3-turbo`.
## Для справки
В Node.js это работает одной строкой:
```js
await fetch('https://api.aillm.ru/v1/chat/completions', {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'gpt-oss-120b', messages: [...] })
});
```
Нужен такой же простой способ в Python.
@@ -0,0 +1,22 @@
# Логика хранения файлов — временная (на период тестирования)
**v1.0.87**
## Как сейчас
- Каждый сеанс (перезагрузка страницы) = новый `contract_id`
- Файлы сохраняются в БД (documents + supplements) навсегда
- При повторной загрузке того же файла в том же контракте — старый удаляется, новый INSERT
- Старые контракты (от предыдущих сеансов) остаются в БД, но юзер их не видит
- Таблица на экране — только файлы текущего сеанса
## Что изменится
- Файлы не должны копиться в БД бесконечно
- Будет «файловая» система: юзер видит что уже есть в БД, может выбрать
- При совпадении имён — спрашивать разрешение на перезапись
- Возможно: хранить файлы только в сессии браузера, БД — только для промптов
## Не хардкодить
Текущая логика удаления-перезаписи (upload.cfm) — временная. При рефакторинге файловой системы пересмотреть.
+102
View File
@@ -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`?
+76
View File
@@ -0,0 +1,76 @@
# Запрос на анализ: XHR onerror («Сеть») при POST /upload с телом > 100KB
**Дата:** 2026-06-17
**Версия:** v1.41
**ТОЛЬКО АНАЛИЗ. КОД НЕ ПРАВИТЬ.**
---
## Что поменялось в понимании
После просмотра текущего кода причина выглядит уже не как `request.get_data()` сама по себе.
Ветка `/upload` сейчас делает так:
1. читает всё тело через `request.get_data(as_text=True)`
2. выдёргивает `filename`, `data`, `cid` regex-ом
3. если `cid` пустой, синхронно создаёт контракт в PostgreSQL
4. затем синхронно вызывает `redis_client.push(...)`
5. только после этого возвращает `jsonify({"contract_id": ...})`
Самое подозрительное место — именно синхронный Redis push в запросе, а не чтение тела. Если Redis/DNS/сеть внутри кластера тормозит или подвисает, клиент уже видит `xhr.onerror` как «Сеть», хотя тело запроса успело уйти целиком.
---
## Текущий код
```python
# app.py
raw = request.get_data(as_text=True)
...
redis_client.push({"filename": filename, "b64": b64, "mime": mime, "cid": str(cid)})
return jsonify({"contract_id": str(cid)})
```
```python
# redis_client.py
_r = redis.Redis(
host=os.getenv("REDIS_HOST", "redisk8s....svc.cluster.local"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASS", "..."),
decode_responses=True,
socket_connect_timeout=5,
)
def push(task: dict):
_get().rpush(UPLOAD_QUEUE, json.dumps(task, ensure_ascii=False))
```
---
## Локальная гипотеза
`request.get_data()` не ломает соединение на 200KB. Запрос уже дошёл.
Соединение рвётся или уходит в таймаут на этапе синхронного `rpush()` в Redis, либо на первом обращении к Redis-клиенту, если DNS/соединение внутри кластера нестабильны. Это объясняет, почему:
- маленькие тела проходят чаще;
- сервер иногда успевает обработать и записать лог;
- `xhr.upload` показывает 100%, а затем приходит `onerror`;
- `curl` даёт плавающий `HTTP:000` / `ReadTimeout`.
---
## Что надо проверить первым
Самый дешёвый дискриминирующий тест — замерить время именно вокруг `redis_client.push()` и временно обойти его для `/upload`.
Если ответ сразу стабилизируется, проблема подтверждается.
Если нет, следующая точка проверки — внешний таймаут/обрыв на ingress или gunicorn, но по текущему коду Redis выглядит ближе всего к критическому узлу.
---
## Вывод
В v1.40 сместили фокус с парсинга тела на более поздний синхронный шаг. Сам `get_data()` теперь выглядит не как корень проблемы, а как просто первая стадия запроса. Корневой кандидат — блокирующий `redis_client.push()` в обработчике `/upload`.