feat: notes-python CRUD example + runtime path/query forwarding
- invoke.go: forward sub-path and query string to function pods - server.js v0.1.2: add _path, _query, _method to event - server.py v0.1.1: add _path, _query, _method to event - upload.go: bump runtime versions (nodejs20:v0.1.2, python3.11:v0.1.1) - examples/notes-python: CRUD notes via sub-path routing - sql-runner: generic SQL executor for DDL jobs - notes: CRUD router (/add, /update, /delete) - notes-list: SELECT all notes - init.tf: create TABLE + INDEX on apply
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
# 2026-03-09
|
||||||
|
# handler.py — возвращает все записи из таблицы notes.
|
||||||
|
# GET/POST /fn/default/notes-list → JSON массив записей, сортировка по created_at DESC.
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
|
||||||
|
def handle(event):
|
||||||
|
dsn = os.environ['PG_DSN']
|
||||||
|
conn = psycopg2.connect(dsn)
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, title, body, created_at::text FROM notes ORDER BY created_at DESC"
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 2026-03-09
|
||||||
|
# handler.py — CRUD роутер для таблицы notes.
|
||||||
|
# Роутинг по event._path (sub-path URL):
|
||||||
|
# POST /fn/default/notes/add?title=...&body=... → INSERT
|
||||||
|
# POST /fn/default/notes/update?id=1&title=... → UPDATE
|
||||||
|
# POST /fn/default/notes/delete?id=1 → DELETE
|
||||||
|
# _path и _query добавляет runtime из HTTP запроса (server.py).
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
|
||||||
|
def handle(event):
|
||||||
|
dsn = os.environ['PG_DSN']
|
||||||
|
# sub-path без ведущего слэша: "add", "update", "delete"
|
||||||
|
action = event.get('_path', '/').strip('/')
|
||||||
|
q = event.get('_query', {})
|
||||||
|
|
||||||
|
conn = psycopg2.connect(dsn)
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
|
||||||
|
if action == 'add':
|
||||||
|
title = q.get('title') or event.get('title', '')
|
||||||
|
body = q.get('body') or event.get('body', '')
|
||||||
|
if not title:
|
||||||
|
return {'error': 'title is required'}
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO notes (title, body) VALUES (%s, %s)"
|
||||||
|
" RETURNING id, title, body, created_at::text",
|
||||||
|
(title, body)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
elif action == 'update':
|
||||||
|
id_ = q.get('id') or event.get('id')
|
||||||
|
if not id_:
|
||||||
|
return {'error': 'id is required'}
|
||||||
|
title = q.get('title') or event.get('title', '')
|
||||||
|
body = q.get('body') or event.get('body', '')
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE notes SET title=%s, body=%s WHERE id=%s"
|
||||||
|
" RETURNING id, title, body, created_at::text",
|
||||||
|
(title, body, int(id_))
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return dict(row) if row else {'error': 'not found'}
|
||||||
|
|
||||||
|
elif action == 'delete':
|
||||||
|
id_ = q.get('id') or event.get('id')
|
||||||
|
if not id_:
|
||||||
|
return {'error': 'id is required'}
|
||||||
|
cur.execute(
|
||||||
|
"DELETE FROM notes WHERE id=%s RETURNING id",
|
||||||
|
(int(id_),)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return {'deleted': row['id']} if row else {'error': 'not found'}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'error': f'unknown action: /{action}',
|
||||||
|
'hint': 'use /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {'error': str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 2026-03-09
|
||||||
|
# handler.py — универсальный исполнитель SQL запросов.
|
||||||
|
# Принимает event.statements — массив SQL строк, выполняет последовательно.
|
||||||
|
# Используется sless_job для DDL операций (CREATE TABLE, миграции и т.д.)
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
|
||||||
|
def handle(event):
|
||||||
|
dsn = os.environ['PG_DSN']
|
||||||
|
statements = event.get('statements', [])
|
||||||
|
if not statements:
|
||||||
|
return {'error': 'no statements provided'}
|
||||||
|
|
||||||
|
conn = psycopg2.connect(dsn)
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
for sql in statements:
|
||||||
|
cur.execute(sql)
|
||||||
|
conn.commit()
|
||||||
|
return {'ok': True, 'executed': len(statements)}
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
return {'error': str(e)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary
|
||||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,34 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# init.tf — джобы инициализации БД: создание таблицы + индекса.
|
||||||
|
# Запускаются один раз при terraform apply.
|
||||||
|
# Для повторного запуска (например после drop) — увеличь run_id.
|
||||||
|
|
||||||
|
resource "sless_job" "create_table" {
|
||||||
|
namespace = "default"
|
||||||
|
name = "notes-create-table"
|
||||||
|
function = sless_function.sql_runner.name
|
||||||
|
wait_timeout_sec = 120
|
||||||
|
run_id = 1
|
||||||
|
|
||||||
|
event_json = jsonencode({
|
||||||
|
statements = [
|
||||||
|
"CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text NOT NULL, body text, created_at timestamp DEFAULT now())"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_job" "create_index" {
|
||||||
|
depends_on = [sless_job.create_table]
|
||||||
|
|
||||||
|
namespace = "default"
|
||||||
|
name = "notes-create-index"
|
||||||
|
function = sless_function.sql_runner.name
|
||||||
|
wait_timeout_sec = 60
|
||||||
|
run_id = 1
|
||||||
|
|
||||||
|
event_json = jsonencode({
|
||||||
|
statements = [
|
||||||
|
"CREATE INDEX IF NOT EXISTS notes_created_idx ON notes(created_at DESC)"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# main.tf — terraform{} + provider для notes-python примера.
|
||||||
|
# Ресурсы вынесены в отдельные .tf файлы.
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
sless = {
|
||||||
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
|
version = "~> 0.1.7"
|
||||||
|
}
|
||||||
|
archive = {
|
||||||
|
source = "hashicorp/archive"
|
||||||
|
version = "~> 2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "sless" {
|
||||||
|
endpoint = "https://sless-api.kube5s.ru"
|
||||||
|
token = "dev-token-change-me"
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# notes-list.tf — функция для получения всех записей из таблицы notes.
|
||||||
|
# GET или POST /fn/default/notes-list → JSON массив всех записей, сортировка по дате (новые первые).
|
||||||
|
|
||||||
|
data "archive_file" "notes_list" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/code/notes-list"
|
||||||
|
output_path = "${path.module}/dist/notes-list.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "notes_list" {
|
||||||
|
namespace = "default"
|
||||||
|
name = "notes-list"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "handler.handle"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PG_DSN = var.pg_dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
code_path = data.archive_file.notes_list.output_path
|
||||||
|
code_hash = filesha256("${path.module}/code/notes-list/handler.py")
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "notes_list_http" {
|
||||||
|
namespace = "default"
|
||||||
|
name = "notes-list-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.notes_list.name
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# notes.tf — CRUD функция для таблицы notes.
|
||||||
|
# Роутинг по sub-path URL:
|
||||||
|
# POST /fn/default/notes/add?title=...&body=... → INSERT записи
|
||||||
|
# POST /fn/default/notes/update?id=1&title=...&body=... → UPDATE записи
|
||||||
|
# POST /fn/default/notes/delete?id=1 → DELETE записи
|
||||||
|
|
||||||
|
data "archive_file" "notes" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/code/notes"
|
||||||
|
output_path = "${path.module}/dist/notes.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "notes" {
|
||||||
|
namespace = "default"
|
||||||
|
name = "notes"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "handler.handle"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PG_DSN = var.pg_dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
code_path = data.archive_file.notes.output_path
|
||||||
|
code_hash = filesha256("${path.module}/code/notes/handler.py")
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "notes_http" {
|
||||||
|
namespace = "default"
|
||||||
|
name = "notes-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.notes.name
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# outputs.tf — URL эндпоинтов для notes-python примера.
|
||||||
|
|
||||||
|
output "notes_url" {
|
||||||
|
value = sless_trigger.notes_http.url
|
||||||
|
description = "Базовый URL CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "notes_list_url" {
|
||||||
|
value = sless_trigger.notes_list_http.url
|
||||||
|
description = "URL для получения всех записей (GET или POST)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# sql-runner.tf — универсальная функция для выполнения SQL запросов.
|
||||||
|
# Используется джобами для DDL операций (CREATE TABLE, индексы, миграции).
|
||||||
|
# event.statements — массив SQL строк, выполняются последовательно в одной транзакции.
|
||||||
|
|
||||||
|
data "archive_file" "sql_runner" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/code/sql-runner"
|
||||||
|
output_path = "${path.module}/dist/sql-runner.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "sql_runner" {
|
||||||
|
namespace = "default"
|
||||||
|
name = "sql-runner"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "handler.handle"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PG_DSN = var.pg_dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
code_path = data.archive_file.sql_runner.output_path
|
||||||
|
code_hash = filesha256("${path.module}/code/sql-runner/handler.py")
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 2025-06-05
|
||||||
|
# variables.tf — входные переменные для notes-python примера.
|
||||||
|
|
||||||
|
variable "pg_dsn" {
|
||||||
|
description = "PostgreSQL DSN для подключения к БД внутри кластера"
|
||||||
|
type = string
|
||||||
|
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
// Изменено: 2026-03-07
|
// Изменено: 2026-03-09
|
||||||
// invoke.go — прокси-обработчик для вызова HTTP-триггеров функций.
|
// invoke.go — прокси-обработчик для вызова HTTP-триггеров функций.
|
||||||
// Маршрут: ANY /fn/{namespace}/{name}
|
// Маршрут: ANY /fn/{namespace}/{name} и /fn/{namespace}/{name}/**
|
||||||
// Не защищён auth-токеном — это публичный эндпоинт для вызова функций.
|
// Не защищён auth-токеном — это публичный эндпоинт для вызова функций.
|
||||||
// Проксирует запрос к ClusterIP Service функции внутри кластера:
|
// Проксирует запрос к ClusterIP Service функции внутри кластера:
|
||||||
// http://{name}.sless-fn-{namespace}.svc.cluster.local:8080
|
// http://{name}.sless-fn-{namespace}.svc.cluster.local:8080
|
||||||
// Функция должна быть задеплоена (HTTP триггер активен).
|
// Sub-path и query string пробрасываются как есть:
|
||||||
|
// /fn/ns/notes/add?title=x → http://notes.sless-fn-ns.svc.../add?title=x
|
||||||
|
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@@ -23,15 +25,24 @@ var httpClient = &http.Client{Timeout: 30 * time.Second}
|
|||||||
|
|
||||||
// InvokeFunction проксирует входящий запрос к Service функции в кластере.
|
// InvokeFunction проксирует входящий запрос к Service функции в кластере.
|
||||||
// Namespace выбирается из пути, имя функции — тоже из пути.
|
// Namespace выбирается из пути, имя функции — тоже из пути.
|
||||||
// Сохраняет метод, тело, и Content-Type заголовок оригинального запроса.
|
// Сохраняет метод, тело, Content-Type, sub-path и query string.
|
||||||
func (h *Handler) InvokeFunction(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) InvokeFunction(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
ns := vars["namespace"]
|
ns := vars["namespace"]
|
||||||
name := vars["name"]
|
name := vars["name"]
|
||||||
|
|
||||||
|
// Вычисляем sub-path после /fn/{namespace}/{name}
|
||||||
|
// Например: /fn/default/notes/add → subPath = /add
|
||||||
|
prefix := "/fn/" + ns + "/" + name
|
||||||
|
subPath := strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
|
||||||
// Внутренний URL к Service функции (DNS внутри кластера)
|
// Внутренний URL к Service функции (DNS внутри кластера)
|
||||||
// Namespace функций: sless-fn-{user-namespace}
|
target := fmt.Sprintf("http://%s.sless-fn-%s.svc.cluster.local:8080%s", name, ns, subPath)
|
||||||
target := fmt.Sprintf("http://%s.sless-fn-%s.svc.cluster.local:8080", name, ns)
|
|
||||||
|
// Пробрасываем query string если есть
|
||||||
|
if r.URL.RawQuery != "" {
|
||||||
|
target += "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём проксируемый запрос с тем же методом и телом
|
// Создаём проксируемый запрос с тем же методом и телом
|
||||||
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body)
|
proxyReq, err := http.NewRequestWithContext(r.Context(), r.Method, target, r.Body)
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ import (
|
|||||||
func runtimeBaseImage(runtime string) (string, error) {
|
func runtimeBaseImage(runtime string) (string, error) {
|
||||||
switch runtime {
|
switch runtime {
|
||||||
case "python3.11":
|
case "python3.11":
|
||||||
return "naeel/sless-runtime-python3.11:v0.1.0", nil
|
return "naeel/sless-runtime-python3.11:v0.1.1", nil
|
||||||
case "nodejs20":
|
case "nodejs20":
|
||||||
return "naeel/sless-runtime-nodejs20:v0.1.1", nil
|
return "naeel/sless-runtime-nodejs20:v0.1.2", nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20)", runtime)
|
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20)", runtime)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Изменено: 2026-03-08
|
// Изменено: 2026-03-09
|
||||||
// HTTP-обёртка для serverless функций на Node.js 20.
|
// HTTP-обёртка для serverless функций на Node.js 20.
|
||||||
// Загружает модуль из SLESS_ENTRYPOINT или handler.js по умолчанию.
|
// Загружает модуль из SLESS_ENTRYPOINT или handler.js по умолчанию.
|
||||||
// Формат SLESS_ENTRYPOINT: "module-name.functionName" (например: handler-http.handle)
|
// Формат SLESS_ENTRYPOINT: "module-name.functionName" (например: handler-http.handle)
|
||||||
@@ -51,6 +51,18 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем метаданные запроса: путь, query params, метод.
|
||||||
|
// _path/_query/_method — зарезервированные поля, не конфликтуют с бизнес-полями.
|
||||||
|
// Позволяет роутить внутри одной функции: event._path === '/add'
|
||||||
|
const urlObj = new URL(req.url, 'http://localhost');
|
||||||
|
const query = {};
|
||||||
|
urlObj.searchParams.forEach((v, k) => { query[k] = v; });
|
||||||
|
if (typeof event === 'object' && !Array.isArray(event)) {
|
||||||
|
event._path = urlObj.pathname;
|
||||||
|
event._query = query;
|
||||||
|
event._method = req.method;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await userHandle(event);
|
const result = await userHandle(event);
|
||||||
sendJSON(res, 200, result);
|
sendJSON(res, 200, result);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Изменено: 2026-03-08
|
# Изменено: 2026-03-09
|
||||||
# HTTP-обёртка для serverless функций на Python 3.11.
|
# HTTP-обёртка для serverless функций на Python 3.11.
|
||||||
# Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию.
|
# Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию.
|
||||||
# Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle)
|
# Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle)
|
||||||
@@ -10,6 +10,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
PORT = 8080
|
PORT = 8080
|
||||||
|
|
||||||
@@ -44,11 +45,25 @@ class FunctionHandler(BaseHTTPRequestHandler):
|
|||||||
format % args,
|
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
|
||||||
|
return event
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/health":
|
if self.path == "/health":
|
||||||
self._respond(200, {"status": "ok"})
|
self._respond(200, {"status": "ok"})
|
||||||
else:
|
else:
|
||||||
self._respond(200, _handle({}))
|
event = self._parse_request_meta({})
|
||||||
|
self._respond(200, _handle(event))
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
length = int(self.headers.get("Content-Length", 0))
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
@@ -58,6 +73,7 @@ class FunctionHandler(BaseHTTPRequestHandler):
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# Если тело не JSON — передаём как строку, не ломаем вызов
|
# Если тело не JSON — передаём как строку, не ломаем вызов
|
||||||
event = {"body": body.decode("utf-8", errors="replace")}
|
event = {"body": body.decode("utf-8", errors="replace")}
|
||||||
|
event = self._parse_request_meta(event)
|
||||||
result = _handle(event)
|
result = _handle(event)
|
||||||
self._respond(200, result)
|
self._respond(200, result)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user