HTTPエンドポイント

http.endpointは、HTTPメソッドとパスをLuaハンドラ関数に対応付けます。

分類:設定およびAPIリファレンス。 YAMLブロックはレジストリの断片であり、参照されるサーバー、ルーター、ミドルウェア、関数エントリ、セキュリティポリシーがすでに存在することを前提としています。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 いいえ 親ルーター(ルーターが1つだけ登録されている場合はそれがデフォルト)
method string はい HTTPメソッド、または任意のメソッドを表す"*"
path string はい URLパスパターン
func registry.ID はい 実行する関数

HTTPメソッド

サポートされるメソッド:

メソッド 用途
GET リソースの取得
POST リソースの作成
PUT リソースの置換
PATCH 部分更新
DELETE リソースの削除
HEAD ヘッダーのみ
OPTIONS CORSプリフライト(自動処理)
TRACE 診断ループバック
* 任意のメソッド

メソッド名は大文字です。methodは必須で、この集合に含まれない値は設定エラーとして拒否されます。

メソッド非依存のエンドポイント

method: "*"はそのパスをすべてのHTTPメソッドに対して登録し、ハンドラはreq:method()で実際のメソッドを読み取ります:

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

通常のエンドポイントでは、ルーターは同じパスにOPTIONSハンドラも登録するため、CORSミドルウェアはエンドポイントを実行せずにプリフライトへ応答できます。*エンドポイントにはそのハンドラは登録されません。すでにOPTIONSにマッチするためです。それでもルーターミドルウェアはこれをラップするため、設定されたCORSミドルウェアは、エンドポイントが実行される前に許可されたプリフライトへ204で応答します。それ以外のOPTIONSリクエストはエンドポイント関数自身に到達し、そこで応答する必要があります。

パスパラメータ

URLパラメータには{param}構文を使用します:

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

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

ハンドラからアクセスする例:

local http = require("http")

local function handler()
    local req, req_err = http.request()
    if req_err then return nil, req_err end
    local user_id, user_err = req:param("user_id")
    if user_err then return nil, user_err end
    local post_id, post_err = req:param("post_id")
    if post_err then return nil, post_err end
    return {user_id = user_id, post_id = post_id}
end

ワイルドカードパス

残りのすべてのパスセグメントに一致させるには{path...}を使用します:

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

このキャッチオールセグメントにより、ルートは/files/docs/readme.mdのようなリクエストにマッチします。キャプチャされた末尾は、末尾のドットを除いた名前で、他のパラメータと同じように読み取れます:

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

ハンドラ関数

エンドポイント関数はhttpモジュールからリクエストオブジェクトとレスポンスオブジェクトを取得します:

local http = require("http")
local funcs = require("funcs")

local function handler()
    local req, req_err = http.request()
    if req_err then return nil, req_err end
    local res, res_err = http.response()
    if res_err then return nil, res_err end

    local user_id, param_err = req:param("id")
    if param_err then return nil, param_err end

    local user, call_err = funcs.call("app.users:get_user", user_id)
    if call_err then return nil, call_err end

    local type_err = res:set_content_type(http.CONTENT.JSON)
    if type_err then return nil, type_err end
    local status_err = res:set_status(http.STATUS.OK)
    if status_err then return nil, status_err end
    local write_err = res:write_json(user)
    if write_err then return nil, write_err end
    return true
end

return { handler = handler }

リクエストオブジェクト

メソッド 戻り値 説明
req:method() string HTTPメソッド
req:path() string リクエストパス
req:param(name) string URLパラメータ
req:params() table すべてのパスパラメータ
req:query(name) string クエリパラメータ
req:query_params() table すべてのクエリパラメータ
req:header(name) string リクエストヘッダー
req:headers() table すべてのリクエストヘッダー
req:body() string リクエストボディ
req:body_json() table, error JSONボディをパース
req:has_body() boolean ボディの有無を確認
req:content_type() string コンテンツタイプ
req:content_length() number ボディサイズ(バイト)
req:host() string ホスト名
req:remote_addr() string クライアントIPアドレス
req:accepts(type) boolean コンテンツネゴシエーション
req:is_content_type(type) boolean コンテンツタイプを確認
req:stream() Stream 大きなファイル用のストリームとしてボディを取得
req:parse_multipart(max?) table, error マルチパートフォームをパース

レスポンスオブジェクト

メソッド 説明
res:set_status(code) HTTPステータスコードを設定。ヘッダー送信済みの場合はエラーを返す
res:set_header(name, value) レスポンスヘッダーを設定。ヘッダー送信済みの場合はエラーを返す
res:set_content_type(type) コンテンツタイプを設定。ヘッダー送信済みの場合はエラーを返す
res:write(data) 生のボディを書き込む。失敗時はエラーを返す
res:write_json(data) JSONレスポンスを書き込む。失敗時はエラーを返す
res:write_event(data) SSEイベントを送信してフラッシュする。失敗時はエラーを返す
res:set_transfer(encoding) chunkedまたはsse転送モードを設定。ヘッダー送信済みの場合はエラーを返す
res:flush() レスポンスをフラッシュし、エラー値を返す

JSON APIパターン

JSON APIハンドラでは、リクエストボディをパースし、不正な入力を拒否して、JSON結果を書き込めます:

local http = require("http")
local funcs = require("funcs")

local function handler()
    local req, req_err = http.request()
    if req_err then return nil, req_err end
    local res, res_err = http.response()
    if res_err then return nil, res_err end

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

    local result, process_err = funcs.call("app.api:process_request", data)
    if process_err then return nil, process_err end

    local status_err = res:set_status(http.STATUS.OK)
    if status_err then return nil, status_err end
    local write_err = res:write_json(result)
    if write_err then return nil, write_err end
    return true
end

return { handler = handler }

エラーレスポンス

local http = require("http")
local funcs = require("funcs")

local function api_error(res, status, code, message)
    local status_err = res:set_status(status)
    if status_err then return nil, status_err end
    local write_err = res:write_json({
        error = {
            code = code,
            message = message
        }
    })
    if write_err then return nil, write_err end
    return true
end

local function handler()
    local req, req_err = http.request()
    if req_err then return nil, req_err end
    local res, res_err = http.response()
    if res_err then return nil, res_err end

    local user_id, param_err = req:param("id")
    if param_err then return nil, param_err end
    local user, err = funcs.call("app.users: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

    local status_err = res:set_status(http.STATUS.OK)
    if status_err then return nil, status_err end
    local write_err = res:write_json(user)
    if write_err then return nil, write_err end
    return true
end

return { handler = handler }

例

CRUDエンドポイント

entries:
  - name: users_router
    kind: http.router
    meta:
      server: gateway
    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

保護されたエンドポイント

認可ミドルウェアはエンドポイントではなく親ルーターに設定します。ポストマッチミドルウェア(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

関連項目