# Запрос на анализ: случайные обрывы соединения при 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 обрывать соединения пула, вызывая каскадный отказ?