HTTP 클라이언트
http_client 모듈은 헤더, 쿼리 파라미터, 폼, 파일 업로드, 인증, TLS 옵션, 스트리밍 응답 및 동시 배치로 HTTP 요청을 보냅니다.
이 페이지는 부분적인 요청 예제를 제공하는 API 레퍼런스입니다. URL, 토큰, 자격 증명, 요청 데이터와 인증서 자료는 애플리케이션이 제공합니다. 예제는 응답을 사용하기 전에 Response, error를 확인하고 스트리밍 본문을 명시적으로 닫습니다.
로딩
local http_client = require("http_client")
모듈을 불러오기 전에 실행 엔트리의 modules: 목록에 http_client를 추가하세요. JSON 및 파일시스템 예제에는 json과 fs도 필요합니다.
HTTP 메서드
모든 메서드는 동일한 시그니처를 공유합니다: method(url, options?) 반환 Response, error.
GET 요청
GET 요청을 보냅니다.
local resp, err = http_client.get("https://api.example.com/users")
if err then
return nil, err
end
print(resp.status_code) -- 200
print(resp.body) -- response body
POST 요청
POST 요청을 보냅니다.
local json = require("json")
local body, body_err = json.encode({name = "Alice", email = "alice@example.com"})
if body_err then return nil, body_err end
local resp, err = http_client.post("https://api.example.com/users", {
headers = {["Content-Type"] = "application/json"},
body = body
})
if err then return nil, err end
PUT 요청
PUT 요청을 보냅니다.
local body, body_err = json.encode({name = "Alice Smith"})
if body_err then return nil, body_err end
local resp, err = http_client.put("https://api.example.com/users/123", {
headers = {["Content-Type"] = "application/json"},
body = body
})
if err then return nil, err end
PATCH 요청
PATCH 요청을 보냅니다.
local body, body_err = json.encode({status = "active"})
if body_err then return nil, body_err end
local resp, err = http_client.patch("https://api.example.com/users/123", {
headers = {["Content-Type"] = "application/json"},
body = body
})
if err then return nil, err end
DELETE 요청
DELETE 요청을 보냅니다.
local resp, err = http_client.delete("https://api.example.com/users/123", {
headers = {["Authorization"] = "Bearer " .. token}
})
if err then return nil, err end
HEAD 요청
HEAD 요청은 응답 본문 없이 헤더만 반환합니다.
local resp, err = http_client.head("https://cdn.example.com/file.zip")
if err then return nil, err end
local size = resp.headers["Content-Length"]
커스텀 메서드
local resp, err = http_client.request("PROPFIND", "https://dav.example.com/folder", {
headers = {["Depth"] = "1"}
})
if err then return nil, err end
| 파라미터 | 타입 | 설명 |
|---|---|---|
method |
string | HTTP 메서드 |
url |
string | 요청 URL |
options |
table | 요청 옵션 (선택적) |
요청 옵션
| 필드 | 타입 | 설명 |
|---|---|---|
headers |
table | 요청 헤더 {["Name"] = "value"} |
body |
string | 요청 본문 |
query |
table | 쿼리 파라미터 {key = "value"} |
form |
table | 폼 데이터 (Content-Type 자동 설정) |
files |
table | 파일 업로드 (파일 정의 배열) |
cookies |
table | 요청 쿠키 {name = "value"} |
auth |
table | Basic auth {user = "name", pass = "secret"} |
timeout |
number/string | 타임아웃: 초 단위 숫자 또는 "30s", "1m" 같은 문자열 |
stream |
boolean | 버퍼링 대신 응답 본문 스트리밍 |
max_response_body |
number | 최대 응답 크기 바이트 (0 = 기본값) |
unix_socket |
string | Unix 소켓 경로로 연결 |
tls |
table | 요청별 TLS 설정 (TLS 옵션 참조) |
overlay_network |
string | 네트워크 오버레이를 통해 라우팅 — network.socks5 / network.tailscale / network.i2p 엔트리의 레지스트리 ID |
쿼리 파라미터
local resp, err = http_client.get("https://api.example.com/search", {
query = {
q = "lua programming",
page = "1",
limit = "20"
}
})
if err then return nil, err end
헤더와 인증
local resp, err = http_client.get("https://api.example.com/data", {
headers = {
["Authorization"] = "Bearer " .. token,
["Accept"] = "application/json"
}
})
if err then return nil, err end
-- Or use basic auth
local resp, err = http_client.get("https://api.example.com/data", {
auth = {user = service_user, pass = service_password}
})
if err then return nil, err end
인증 값은 애플리케이션이 소유한 비밀 저장소에서 읽고 TLS를 통해서만 전송하세요.
폼 데이터
local resp, err = http_client.post("https://api.example.com/login", {
form = {
username = username,
password = password
}
})
if err then return nil, err end
파일 업로드
local resp, err = http_client.post("https://api.example.com/upload", {
form = {title = "My Document"},
files = {
{
name = "attachment", -- form field name
filename = "report.pdf", -- original filename
content = pdf_data, -- file content
content_type = "application/pdf"
}
}
})
if err then return nil, err end
| 파일 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
string | 예 | 폼 필드 이름 |
filename |
string | 아니오 | 원본 파일명 |
content |
string | 예* | 파일 내용 |
reader |
userdata | 예* | 대안: 내용용 io.Reader |
content_type |
string | 아니오 | 현재 무시됨: 이 필드와 무관하게 업로드되는 각 파트는 항상 Content-Type: application/octet-stream으로 전송됨 |
*content 또는 reader 중 하나가 필수입니다.
고정된 런타임은 전송 전에 reader를 메모리로 모두 읽고 닫지 않으며, EOF가 아닌 읽기 실패를 별도로 노출하지 않습니다. 이미 크기가 제한된 데이터에는 content를 사용하고 요청 후 호출자가 소유한 reader를 닫으세요. content_type 필드는 파싱되지만 런타임 v0.3.32a에서 전달되지 않습니다.
reader 기반 파일은 이 릴리스에서 단일 요청 호출만 지원합니다. request_batch는 content를 전달하지만 파싱된 reader는 버리므로 배치 파일 업로드는 files[].content를 제공해야 합니다.
타임아웃
-- Number: seconds
local resp, err = http_client.get(url, {timeout = 30})
if err then return nil, err end
-- String alternatives use Go duration format: "30s", "1m30s", or "1h".
TLS 옵션 {id="tls-options"}
mTLS(상호 TLS) 및 커스텀 CA 인증서를 위한 요청별 TLS 설정을 구성합니다.
| 필드 | 타입 | 설명 |
|---|---|---|
cert |
string | PEM 형식의 클라이언트 인증서 |
key |
string | PEM 형식의 클라이언트 개인 키 |
ca |
string | PEM 형식의 커스텀 CA 인증서 |
server_name |
string | SNI 검증을 위한 서버 이름 |
insecure_skip_verify |
boolean | TLS 인증서 검증 건너뛰기 |
mTLS를 위해서는 cert와 key를 함께 제공해야 합니다. ca 필드는 시스템 인증서 풀을 커스텀 CA로 대체합니다.
mTLS 인증
local fs = require("fs")
local certs, volume_err = fs.get("app:certs")
if volume_err then return nil, volume_err end
local cert_pem, cert_err = certs:readfile("client.crt")
if cert_err then return nil, cert_err end
local key_pem, key_err = certs:readfile("client.key")
if key_err then return nil, key_err end
local resp, err = http_client.get("https://secure.example.com/api", {
tls = {
cert = cert_pem,
key = key_pem,
}
})
if err then return nil, err end
insecure_skip_verify는 제어된 진단 엔드포인트에서만 사용하세요. 인증서 체인과 호스트 이름 검증을 모두 비활성화합니다.
커스텀 CA
local fs = require("fs")
local certs, volume_err = fs.get("app:certs")
if volume_err then return nil, volume_err end
local ca_pem, ca_err = certs:readfile("internal-ca.crt")
if ca_err then return nil, ca_err end
local resp, err = http_client.get("https://internal.example.com/api", {
tls = {
ca = ca_pem,
server_name = "internal.example.com",
}
})
if err then return nil, err end
안전하지 않은 검증 건너뛰기
개발 환경에서 TLS 검증을 건너뜁니다. http_client.insecure_tls 보안 권한이 필요합니다.
local resp, err = http_client.get("https://localhost:8443/api", {
tls = {
insecure_skip_verify = true,
}
})
if err then return nil, err end
응답 객체
| 필드 | 타입 | 설명 |
|---|---|---|
status_code |
number | HTTP 상태 코드 |
body |
string | 응답 본문 (스트리밍이 아닌 경우) |
body_size |
number | 본문 크기 바이트 (스트리밍이면 -1) |
headers |
table | 응답 헤더 |
cookies |
table | 응답 쿠키 |
url |
string | 최종 URL (리다이렉트 후) |
stream |
Stream | 스트림 객체 (stream = true인 경우) |
local resp, err = http_client.get("https://api.example.com/data")
if err then
return nil, err
end
if resp.status_code == 200 then
local data, decode_err = json.decode(resp.body)
if decode_err then return nil, decode_err end
print("Content-Type:", resp.headers["Content-Type"])
end
스트리밍 응답
대용량 응답의 경우, 전체 본문을 메모리에 로드하지 않도록 스트리밍을 사용합니다.
local resp, err = http_client.get("https://cdn.example.com/large-file.zip", {
stream = true
})
if err then
return nil, err
end
-- Process in chunks
local read_err
while true do
local chunk
chunk, read_err = resp.stream:read(65536)
if read_err or not chunk then break end
-- process chunk
end
local _, close_err = resp.stream:close()
if read_err then return nil, read_err end
if close_err then return nil, close_err end
| 스트림 메서드 | 반환 | 설명 |
|---|---|---|
read(n?) |
string, error | 최대 n 바이트 읽기 (기본값: 구현 버퍼) |
close() |
boolean, error | 스트림 닫기 |
resp.stream은 완전한 스트림 객체입니다 — seek, stat, scanner도 사용할 수 있습니다. 스트리밍 응답 본문은 호출자가 소유하며 모든 종료 경로에서 닫아야 합니다.
배치 요청
request_batch는 여러 요청을 동시에 실행합니다.
local requests = {
{"GET", "https://api.example.com/users"},
{"GET", "https://api.example.com/products"},
{"POST", "https://api.example.com/log", {body = "event"}}
}
local responses, batch_errors = http_client.request_batch(requests)
if not responses then
return nil, batch_errors -- whole-batch dispatch or validation failure
end
if batch_errors then
for i = 1, #requests do
local err = batch_errors[i]
if err then
print("Request " .. i .. " failed:", err)
end
end
else
-- All succeeded
for i, resp in ipairs(responses) do
print("Response " .. i .. ":", resp.status_code)
end
end
| 파라미터 | 타입 | 설명 |
|---|---|---|
requests |
table | {method, url, options?} 배열 |
반환: responses, errors - 요청 위치별로 인덱싱된 배열
참고:
- 요청은 동시에 실행됨
- 배치에서는 스트리밍(
stream = true)이 지원되지 않음 - reader 기반 파일 업로드는 배치에서 지원되지 않으므로
files[].content사용 - 결과 배열은 요청 순서와 일치 (1-인덱싱)
URL 인코딩
인코딩
local encoded = http_client.encode_uri("hello world")
-- "hello+world"
local url = "https://api.example.com/search?q=" .. http_client.encode_uri(query)
디코딩
http_client.encode_uri로 인코딩한 문자열을 디코딩합니다.
local decoded, err = http_client.decode_uri("hello+world")
if err then return nil, err end
-- "hello world"
권한
HTTP 요청은 보안 정책 평가 대상입니다.
보안 액션
| 액션 | 리소스 | 설명 |
|---|---|---|
http_client.request |
URL | 특정 URL에 대한 요청 허용/거부 |
http_client.unix_socket |
소켓 경로 | Unix 소켓 연결 허용/거부 |
http_client.private_ip |
IP 주소 | 사설 IP 범위 접근 허용/거부 |
http_client.insecure_tls |
URL | 안전하지 않은 TLS 허용/거부 (검증 건너뛰기) |
network.select |
네트워크 엔트리 ID | 요청에 지정된 overlay_network를 통한 라우팅 허용/거부 |
접근 확인
local security = require("security")
if security.can("http_client.request", "https://api.example.com/users") then
local resp, request_err = http_client.get("https://api.example.com/users")
if request_err then return nil, request_err end
end
SSRF 보호
공인 IP가 아닌 범위는 기본적으로 차단됩니다. 접근하려면 해당 주소에 대한 http_client.private_ip 권한이 필요합니다:
- 루프백, 사설(10.x, 172.16-31.x, 192.168.x), 링크 로컬 유니캐스트 및 멀티캐스트, unspecified 주소
- 캐리어 그레이드 NAT
100.64.0.0/10,192.0.0.0/24, 멀티캐스트224.0.0.0/4, 예약240.0.0.0/4 - 문서화 및 벤치마킹 범위
192.0.2.0/24,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,2001:db8::/32 - IPv6 멀티캐스트
ff00::/8
local resp, err = http_client.get("http://192.168.1.1/admin")
-- Error: not allowed: private IP 192.168.1.1
검사는 URL 문자열이 아니라 다이얼 시점에 수행되며, 호스트가 해석되는 모든 주소를 대상으로 합니다. 여러 주소로 해석되는 호스트 이름은 주소별로 검사됩니다: 거부된 주소는 건너뛰고 다음 주소를 시도하며, 모든 후보가 거부되거나 도달 불가능할 때만 요청이 실패합니다. 따라서 사설 주소로 해석되는 공개 호스트 이름은 사설 IP 리터럴과 정확히 동일하게 차단됩니다.
리다이렉트
리다이렉트는 최대 아홉 번까지 따라가며, 열 번째는 stopped after 10 redirects로 실패합니다. 이 횟수에는 원래 요청이 포함됩니다.
모든 홉은 개별적으로 인가됩니다. 리다이렉트를 따라가기 전에 클라이언트는 대상 URL에 대해 http_client.request를 평가하고 사설 IP 검사를 적용하므로, 허용된 URL을 리다이렉트로 이용해 거부된 URL에 도달할 수 없습니다. 둘 중 하나라도 실패하는 홉은 요청을 중단시킵니다.
정책 설정은 보안 모델을 참조하세요.
에러
| 조건 | 종류 | 재시도 가능 |
|---|---|---|
| 보안 정책 거부 | errors.PERMISSION_DENIED |
아니오 |
| 사설 IP 차단 | errors.PERMISSION_DENIED |
아니오 |
| Unix 소켓 거부 | errors.PERMISSION_DENIED |
아니오 |
| 안전하지 않은 TLS 거부 | errors.PERMISSION_DENIED |
아니오 |
| 잘못된 배치 항목, 배치 스트리밍 또는 잘못된 URI escape | errors.INVALID |
아니오 |
| 컨텍스트 없음 | errors.INTERNAL |
아니오 |
| 잘못된 전송 URL 또는 네트워크 실패 | errors.INTERNAL |
예 |
| 타임아웃 | errors.INTERNAL |
예 |
지원되지 않는 옵션 값은 구조화된 에러 대신 무시되는 경우가 많습니다. 잘못된 Lua 인자 타입과 빈 배치는 Lua 인자 에러를 발생시킵니다.
local resp, err = http_client.get(url)
if err then
if errors.is(err, errors.PERMISSION_DENIED) then
print("Access denied:", err:message())
elseif err:retryable() then
print("Temporary error:", err:message())
end
return nil, err
end
에러 처리는 에러 처리를 참조하세요.