HTTP 엔드포인트
엔드포인트(http.endpoint)는 Lua 함수를 실행하는 HTTP 라우트 핸들러를 정의합니다.
정의
- 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 프리플라이트 (자동 처리) |
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
와일드카드 경로
catch-all 세그먼트는 /files/docs/readme.md 같은 요청을 매칭합니다. 이 요청에서 req:param("path")는 docs/readme.md를 반환합니다.
{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 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 }
Request 객체
| 메서드 | 반환값 | 설명 |
|---|---|---|
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:port 형식의 클라이언트 주소 |
req:accepts(type) |
boolean | 콘텐츠 협상 |
req:is_content_type(type) |
boolean | 콘텐츠 타입 확인 |
req:stream() |
Stream | 대용량 파일용 스트림으로 본문 |
req:parse_multipart(max?) |
table, error | 멀티파트 폼 파싱 |
Response 객체
| 메서드 | 설명 |
|---|---|
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의 일반적인 패턴:
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 }
에러 응답
인가 미들웨어는 엔드포인트가 아니라 부모 라우터에 설정합니다. endpoint_firewall 같은 매칭 후 미들웨어는 라우트 매칭 뒤 라우터의 모든 엔드포인트에 적용됩니다.
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