Код вокруг каждого обработчика
Промежуточный слой (middleware) — это код, который выполняется вокруг ваших обработчиков: до того, как запрос дойдёт до них, после того, как ответ покинет их, или вместо них. Логирование, аутентификация, ограничение частоты запросов, сжатие — всё это промежуточные слои.
Одна и та же идея встречается под тремя названиями: веб-фреймворки говорят middleware, gRPC говорит interceptor, некоторые экосистемы говорят filter. Одно понятие — три словаря.
В Python промежуточный слой — это функция, которая принимает функцию-обработчик и возвращает обёрнутую — именно для этого и создан собственный синтаксис Python @decorator, и это в точности паттерн Декоратор из группы паттернов проектирования. Стопка таких слоёв ведёт себя как Цепочка обязанностей.
Промежуточный слой для логирования
with_log оборачивает любую функцию-обработчик: выводит входящий запрос, даёт обработчику выполнить работу и снова выводит строку на выходе. Сам обработчик об этом ничего не знает. functools.wraps сохраняет видимыми имя и документацию обёрнутой функции — это тот же помощник, который использует каждый декоратор в Python.
Обернуть один раз — логировать всё
Обёрнутый обработчик обслуживает два разных пути, и оба попадают в лог — логирование написано один раз, в обёртке, а не в каждом обработчике.
Вывод: полная программа на Python
import functools
import threading
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def app(method, path):
if path == "/users":
return "Ana, Ben\n"
if path == "/notes":
return "shopping, ideas\n"
return "nothing here\n"
# Промежуточный слой: принимает функцию-обработчик и возвращает обёрнутую.
def with_log(next_handler):
@functools.wraps(next_handler)
def wrapped(method, path):
print("-> ", method, path)
body = next_handler(method, path)
print("<- готово")
return body
return wrapped
handle = with_log(app)
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = handle("GET", self.path)
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(body.encode())
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
for path in ["/users", "/notes"]:
with urllib.request.urlopen(base + path) as resp:
print("тело:", resp.read().decode().strip())
server.shutdown()
Промежуточный слой, который останавливает запрос
Промежуточный слой может также отказать: проверка аутентификации, которая не находит действительного ключа, отвечает 401 Unauthorized и никогда не вызывает следующий обработчик.
В цепочке with_log(with_auth(report)) запрос проходит через слои по порядку — а ответ всплывает обратно через них же.
Слой аутентификации в цепочке
Запрос без ключа останавливается слоем аутентификации с кодом 401 — обработчик так и не запускается. С ключом запрос проходит всю цепочку и добирается до данных.
Вывод: полная программа на Python
import threading
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def report(method, path, headers):
return 200, "секретный отчёт\n"
def with_log(next_handler):
def wrapped(method, path, headers):
print("->", method, path)
return next_handler(method, path, headers)
return wrapped
# Промежуточный слой аутентификации: может остановить запрос до обработчика.
def with_auth(next_handler):
def wrapped(method, path, headers):
if headers.get("X-API-Key") != "secret123":
return 401, "ключ API отсутствует или неверен\n"
return next_handler(method, path, headers)
return wrapped
handle = with_log(with_auth(report))
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
status, body = handle("GET", self.path, self.headers)
self.send_response(status)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(body.encode())
def log_message(self, fmt, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
base = f"http://127.0.0.1:{server.server_address[1]}"
threading.Thread(target=server.serve_forever, daemon=True).start()
for key in ["", "secret123"]:
req = urllib.request.Request(base + "/report")
if key:
req.add_header("X-API-Key", key)
try:
with urllib.request.urlopen(req) as resp:
status, body = resp.status, resp.read().decode()
except urllib.error.HTTPError as err:
status, body = err.code, err.read().decode()
print(" статус:", status, "-", body.strip())
server.shutdown()