Files
sless/runtimes/python3.11/server.py
T
Naeel d286d92a05 fix: invoke.go — forward Content-Length to proxied request (form POST fix)
Without ContentLength, Python BaseHTTPRequestHandler read 0 bytes from body.
operator v0.1.37, python runtime v0.1.4, pg-table-writer HTML form
2026-03-19 08:58:35 +03:00

136 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Изменено: 2026-03-18 (string return → text/plain без json.dumps)
# HTTP-обёртка для serverless функций на Python 3.11.
# Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию.
# Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle)
# Почему importlib: нужно загружать модуль из фиксированного пути вне sys.path.
import sys
import os
import json
import importlib.util
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
PORT = 8080
# Разбираем SLESS_ENTRYPOINT="module.func" → файл и имя функции.
# Fallback: handler.py + handle — для обратной совместимости.
_entrypoint = os.environ.get("SLESS_ENTRYPOINT", "handler.handle")
_dot_idx = _entrypoint.rfind(".")
_module_name = _entrypoint[:_dot_idx] if _dot_idx >= 0 else _entrypoint
_func_name = _entrypoint[_dot_idx + 1:] if _dot_idx >= 0 else "handle"
HANDLER_PATH = f"/app/function/{_module_name}.py"
def load_handler():
# Загружаем модуль пользователя динамически — путь известен только в runtime.
spec = importlib.util.spec_from_file_location(_module_name, HANDLER_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, _func_name):
raise AttributeError(f"{HANDLER_PATH} must define a '{_func_name}(event)' function")
return getattr(module, _func_name)
_handle = load_handler()
class FunctionHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
# Логи в stderr чтобы не мешать stdout
sys.stderr.write("%s - [%s] %s\n" % (
self.address_string(),
self.log_date_time_string(),
format % args,
))
def _parse_request_meta(self, event):
# Добавляем _path, _query, _method в event — позволяет роутить внутри функции.
# Например: /fn/ns/notes/add?title=x → event._path='/add', event._query={'title':'x'}
# Зарезервированные поля, не конфликтуют с бизнес-полями.
parsed = urlparse(self.path)
query = {k: v[0] if len(v) == 1 else v
for k, v in parse_qs(parsed.query).items()}
if isinstance(event, dict):
event['_path'] = parsed.path
event['_query'] = query
event['_method'] = self.command
# Accept заголовок — позволяет функции различать браузер и curl/API.
event['_accept'] = self.headers.get('Accept', '')
return event
def do_GET(self):
if self.path == "/health":
self._respond(200, {"status": "ok"})
else:
event = self._parse_request_meta({})
self._respond(200, _handle(event))
def _handle_with_body(self):
# Общий обработчик для методов с телом (POST, PUT, PATCH, DELETE и др.)
# Читаем тело только если Content-Length > 0, иначе передаём пустой event.
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length) if length > 0 else b""
try:
event = json.loads(body) if body else {}
except json.JSONDecodeError:
# Если тело не JSON — передаём как строку, не ломаем вызов
event = {"body": body.decode("utf-8", errors="replace")}
event = self._parse_request_meta(event)
self._respond(200, _handle(event))
def do_POST(self):
self._handle_with_body()
# PUT, DELETE, PATCH — передаём в функцию как обычные вызовы.
# event['_method'] позволяет функции роутить по методу внутри.
do_PUT = _handle_with_body
do_DELETE = _handle_with_body
do_PATCH = _handle_with_body
def do_HEAD(self):
# HEAD: те же заголовки что и GET, но без тела (HTTP-стандарт).
if self.path == "/health":
result = {"status": "ok"}
else:
result = _handle(self._parse_request_meta({}))
body = json.dumps(result).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
# Тело не отправляем — семантика HEAD
def send_error(self, code, message=None, explain=None):
# Переопределяем дефолтный HTML-ответ на JSON — пользователь всегда получает JSON.
body = json.dumps({"error": message or f"HTTP {code}", "code": code}).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _respond(self, status, data):
# Если функция вернула строку:
# начинается с '<' → HTML (text/html) — для страниц с формами.
# иначе → text/plain — для человекочитаемого текста.
# Позволяет функциям возвращать HTML напрямую без изменений runtime-образа.
if isinstance(data, str):
body = data.encode("utf-8")
ctype = "text/html; charset=utf-8" if data.lstrip().startswith("<") else "text/plain; charset=utf-8"
else:
body = json.dumps(data).encode("utf-8")
ctype = "application/json"
self.send_response(status)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), FunctionHandler)
print(f"sless runtime (python3.11) listening on :{PORT}", flush=True)
server.serve_forever()