HTTP-эндпоинты

Эндпоинты (http.endpoint) определяют обработчики HTTP-маршрутов, которые выполняют Lua-функции.

Определение

- name: get_user
  kind: http.endpoint
  meta:
    router: app:api_router
  method: GET
  path: /users/{id}
  func: app.users:get_user

Конфигурация

Поле Тип Обязательно Описание
meta.router registry.ID Нет Родительский роутер (по умолчанию используется единственный зарегистрированный роутер, если он один)
method string Да HTTP-метод либо "*" для любого метода
path string Да Шаблон URL-пути
func registry.ID Да Выполняемая функция

HTTP-методы

Поддерживаемые методы:

Метод Назначение
GET Получение ресурсов
POST Создание ресурсов
PUT Замена ресурсов
PATCH Частичное обновление
DELETE Удаление ресурсов
HEAD Только заголовки
OPTIONS CORS preflight (автоматически)
TRACE Диагностический loopback
* Любой метод

Имена методов пишутся в верхнем регистре; method обязателен, а любое значение вне этого набора отклоняется как ошибка конфигурации.

Эндпоинты, не зависящие от метода

method: "*" регистрирует путь для всех HTTP-методов, а обработчик читает фактический метод через req:method():

- name: proxy
  kind: http.endpoint
  method: "*"
  path: /proxy/{path...}
  func: proxy_handler

Для обычного эндпоинта роутер дополнительно регистрирует обработчик OPTIONS на том же пути, поэтому CORS-middleware может ответить на preflight, не запуская эндпоинт. Эндпоинт с * такого обработчика не получает: он и так совпадает с OPTIONS. Middleware роутера по-прежнему оборачивает его, поэтому настроенное CORS-middleware отвечает на разрешённый preflight кодом 204 до запуска эндпоинта; любой другой запрос OPTIONS доходит до самой функции эндпоинта, которая и должна на него отвечать.

Параметры пути

Используйте синтаксис {param} для URL-параметров:

- name: get_user
  kind: http.endpoint
  method: GET
  path: /users/{id}
  func: get_user

- name: get_user_post
  kind: http.endpoint
  method: GET
  path: /users/{user_id}/posts/{post_id}
  func: get_user_post

Доступ в обработчике:

local http = require("http")

local function handler()
    local req = http.request()
    local user_id = req:param("id")
    local post_id = req:param("post_id")
end

Wildcard-пути

Захват оставшегося пути через {path...}:

- name: file_handler
  kind: http.endpoint
  method: GET
  path: /files/{path...}
  func: serve_file

Этот catch-all-сегмент заставляет маршрут совпадать с запросами вида /files/docs/readme.md. Захваченный хвост читается как любой другой параметр — под именем без завершающих точек:

local req = http.request()
local tail = req:param("path")  -- "docs/readme.md"

Функция-обработчик

Функции эндпоинтов получают объекты запроса и ответа из модуля http:

local http = require("http")
local json = require("json")

local function handler()
    local req = http.request()
    local res = http.response()

    -- Чтение запроса
    local body = req:body()
    local user_id = req:param("id")
    local page = req:query("page")
    local auth = req:header("Authorization")

    -- Обработка
    local user = get_user(user_id)

    -- Запись ответа
    res:set_content_type(http.CONTENT.JSON)
    res:set_status(http.STATUS.OK)
    res:write_json(user)
end

return { handler = handler }

Объект Request

Метод Возвращает Описание
req:method() string HTTP-метод
req:path() string Путь запроса
req:param(name) string URL-параметр
req:params() table Все параметры пути
req:query(name) string Query-параметр
req:query_params() table Все query-параметры
req:header(name) string Заголовок запроса
req:headers() table Все заголовки запроса
req:body() string Тело запроса
req:body_json() table, error Разбор JSON-тела
req:has_body() boolean Проверка наличия тела
req:content_type() string Content-Type
req:content_length() number Размер тела в байтах
req:host() string Имя хоста
req:remote_addr() string IP-адрес клиента
req:accepts(type) boolean Content negotiation
req:is_content_type(type) boolean Проверка content-type
req:stream() Stream Тело как поток для больших файлов
req:parse_multipart(max?) table, error Разбор multipart-формы

Объект Response

Метод Описание
res:set_status(code) Установить HTTP-статус
res:set_header(name, value) Установить заголовок ответа
res:set_content_type(type) Установить content-type
res:write(data) Записать raw-тело
res:write_json(data) Записать JSON-ответ
res:write_event(data) Отправить SSE-событие
res:set_transfer(encoding) Установить режим передачи (SSE, chunked)
res:flush() Сбросить ответ клиенту

Паттерн JSON API

Типовой паттерн для JSON API:

local http = require("http")

local function handler()
    local req = http.request()
    local res = http.response()

    local data, err = req:body_json()
    if err then
        res:set_status(http.STATUS.BAD_REQUEST)
        res:write_json({error = "Invalid JSON"})
        return
    end

    local result = process(data)

    res:set_status(http.STATUS.OK)
    res:write_json(result)
end

return { handler = handler }

Ответы об ошибках

local http = require("http")

local function api_error(res, status, code, message)
    res:set_status(status)
    res:write_json({
        error = {
            code = code,
            message = message
        }
    })
end

local function handler()
    local req = http.request()
    local res = http.response()

    local user_id = req:param("id")
    local user, err = db.get_user(user_id)

    if err then
        if errors.is(err, errors.NOT_FOUND) then
            return api_error(res, http.STATUS.NOT_FOUND, "USER_NOT_FOUND", "User not found")
        end
        return api_error(res, http.STATUS.INTERNAL_ERROR, "INTERNAL_ERROR", "Server error")
    end

    res:set_status(http.STATUS.OK)
    res:write_json(user)
end

return { handler = handler }

Примеры

CRUD-эндпоинты

entries:
  - name: users_router
    kind: http.router
    prefix: /api/users
    middleware:
      - cors
      - compress

  - name: list_users
    kind: http.endpoint
    meta:
      router: users_router
    method: GET
    path: /
    func: app.users:list

  - name: get_user
    kind: http.endpoint
    meta:
      router: users_router
    method: GET
    path: /{id}
    func: app.users:get

  - name: create_user
    kind: http.endpoint
    meta:
      router: users_router
    method: POST
    path: /
    func: app.users:create

  - name: update_user
    kind: http.endpoint
    meta:
      router: users_router
    method: PUT
    path: /{id}
    func: app.users:update

  - name: delete_user
    kind: http.endpoint
    meta:
      router: users_router
    method: DELETE
    path: /{id}
    func: app.users:delete

Защищённый эндпоинт

Middleware авторизации настраивается на родительском роутере, а не на эндпоинте. Post-match middleware (например, endpoint_firewall) выполняется после сопоставления маршрута и применяется ко всем эндпоинтам роутера:

- name: admin_router
  kind: http.router
  meta:
    server: gateway
  prefix: /admin
  middleware:
    - cors
    - token_auth
  post_middleware:
    - endpoint_firewall
  post_options:
    endpoint_firewall.action: "admin"

- name: admin_endpoint
  kind: http.endpoint
  meta:
    router: admin_router
  method: POST
  path: /settings
  func: app.admin:update_settings

См. также