HTTP
Handle HTTP requests and build responses. Access request data, route parameters, headers, and body content. Build responses with status codes, headers, and streaming support.
For server configuration, see HTTP Server.
Loading
local http = require("http")
Accessing the Request
Get the current HTTP request context:
local req = http.request()
-- With options
local req = http.request({
timeout = 5000, -- 5 second body read timeout
max_body = 10485760 -- 10MB max body
})
| Parameter | Type | Description |
|---|---|---|
options.timeout |
integer | Body read timeout in ms (default: 300000 / 5 min) |
options.max_body |
integer | Max body size in bytes (default: 120MB) |
Returns: Request, error
Accessing the Response
Get the current HTTP response context:
local res = http.response()
Returns: Response, error
Request Methods
method
local method = req:method()
if method == http.METHOD.GET then
return get_resource(id)
elseif method == http.METHOD.POST then
return create_resource(req:body_json())
elseif method == http.METHOD.PUT then
return update_resource(id, req:body_json())
elseif method == http.METHOD.DELETE then
return delete_resource(id)
end
path
local path = req:path()
print(path) -- "/api/users/123"
-- Route based on path
if path:match("^/api/") then
return handle_api(req)
end
query
Gets a single query parameter.
-- GET /search?q=hello&page=2&limit=10
local query = req:query("q") -- "hello"
local page = req:query("page") -- "2"
local missing = req:query("foo") -- nil
-- With defaults
local page = tonumber(req:query("page")) or 1
local limit = tonumber(req:query("limit")) or 20
local sort = req:query("sort") or "created_at"
query_params
Gets all query parameters. Multiple values for the same key are joined with commas.
-- GET /search?tags=lua&tags=go&active=true
local params = req:query_params()
-- {tags = "lua,go", active = "true"}
for key, value in pairs(params) do
print(key .. ": " .. value)
end
header
local auth = req:header("Authorization")
if not auth then
res:set_status(401)
return res:write_json({error = "Missing authorization"})
end
local user_agent = req:header("User-Agent")
local correlation_id = req:header("X-Correlation-ID") or uuid.v4()
content_type
Gets the Content-Type header.
local ct = req:content_type() -- "application/json; charset=utf-8" or nil
content_length
Gets the Content-Length header value.
local length = req:content_length() -- number of bytes
host
Gets the Host header.
local host = req:host() -- "example.com:8080"
param
Gets URL route parameters (from path patterns like /users/:id).
-- Route: /users/:id/posts/:post_id
local user_id = req:param("id")
local post_id = req:param("post_id")
-- Validate parameter
local id = req:param("id")
if not id or not uuid.validate(id) then
res:set_status(400)
return res:write_json({error = "Invalid ID format"})
end
params
Gets all route parameters.
-- Route: /orgs/:org/repos/:repo/issues/:issue
local p = req:params()
-- {org = "acme", repo = "widget", issue = "123"}
local issue = get_issue(p.org, p.repo, p.issue)
body
Reads the full request body as string.
local body = req:body()
-- Parse XML manually
if req:is_content_type("application/xml") then
local data = parse_xml(body)
end
-- Log raw body for debugging
logger.debug("Request body", {body = body, length = #body})
body_json
Reads and parses body as JSON.
local data, err = req:body_json()
if err then
res:set_status(400)
return res:write_json({error = "Invalid JSON: " .. err:message()})
end
-- Validate required fields
if not data.name or not data.email then
res:set_status(400)
return res:write_json({error = "Missing required fields"})
end
local user = create_user(data)
has_body
if req:has_body() then
local data = req:body_json()
process(data)
else
res:set_status(400)
return res:write_json({error = "Request body required"})
end
is_content_type
if not req:is_content_type("application/json") then
res:set_status(415)
return res:write_json({error = "Content-Type must be application/json"})
end
accepts
if req:accepts("application/json") then
res:write_json(data)
elseif req:accepts("text/html") then
res:set_content_type("text/html")
res:write(render_html(data))
else
res:set_status(406)
res:write_json({error = "Cannot produce acceptable response"})
end
remote_addr
local addr = req:remote_addr() -- "192.168.1.100:54321"
-- Extract IP only
local ip = addr:match("^([^:]+)")
-- Rate limiting by IP
if rate_limiter:is_limited(ip) then
res:set_status(429)
return res:write_json({error = "Too many requests"})
end
parse_multipart
Parses multipart form data (file uploads).
local form, err = req:parse_multipart()
if err then
res:set_status(400)
return res:write_json({error = "Invalid form data"})
end
-- Access form values
local title = form.values.title
local description = form.values.description
-- Access uploaded files
if form.files.avatar then
local file = form.files.avatar[1]
local filename = file:name() -- "photo.jpg"
local size = file:size() -- 102400
local content_type = file:header("Content-Type") -- "image/jpeg"
-- Read file content
local stream = file:stream()
local content = stream:read_all()
stream:close()
-- Save to storage
storage.write("avatars/" .. filename, content)
end
-- Handle multiple files
if form.files.documents then
for _, file in ipairs(form.files.documents) do
process_document(file)
end
end
stream
Gets request body as a stream for large files.
local stream = req:stream()
-- Process in chunks
while true do
local chunk, err = stream:read(65536) -- 64KB chunks
if err or not chunk then break end
process_chunk(chunk)
end
stream:close()
Response Methods
set_status
res:set_status(200)
res:set_status(http.STATUS.CREATED)
-- Common patterns
res:set_status(201) -- Created
res:set_status(204) -- No Content (for DELETE)
res:set_status(400) -- Bad Request
res:set_status(401) -- Unauthorized
res:set_status(403) -- Forbidden
res:set_status(404) -- Not Found
res:set_status(500) -- Internal Server Error
set_header
res:set_header("X-Request-ID", correlation_id)
res:set_header("Cache-Control", "max-age=3600")
res:set_header("X-RateLimit-Remaining", tostring(remaining))
-- CORS headers
res:set_header("Access-Control-Allow-Origin", "*")
res:set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
res:set_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
set_content_type
res:set_content_type("application/json")
res:set_content_type(http.CONTENT.JSON)
res:set_content_type("text/html; charset=utf-8")
res:set_content_type("application/pdf")
write
Writes to response body.
res:write("Hello, World!")
-- Build response incrementally
res:write("<html><body>")
res:write("<h1>Title</h1>")
res:write("<p>Content</p>")
res:write("</body></html>")
write_json
Encodes value as JSON and writes it.
-- Success response
res:set_status(200)
res:write_json({
data = users,
total = count,
page = page
})
-- Error response
res:set_status(400)
res:write_json({
error = "Validation failed",
details = {
{field = "email", message = "Invalid format"},
{field = "age", message = "Must be positive"}
}
})
flush
Flushes buffered data to client.
set_transfer
Sets transfer encoding for streaming.
-- Chunked transfer
res:set_transfer(http.TRANSFER.CHUNKED)
for chunk in get_chunks() do
res:write(chunk)
res:flush()
end
-- Server-Sent Events
res:set_transfer(http.TRANSFER.SSE)
write_event
Writes a Server-Sent Event.
-- Real-time updates
res:set_transfer(http.TRANSFER.SSE)
res:write_event({name = "connected", data = {client_id = client_id}})
for progress in task:progress() do
res:write_event({name = "progress", data = {percent = progress}})
end
res:write_event({name = "complete", data = {result = result}})
-- Chat messages
res:write_event({name = "message", data = {
from = "alice",
text = "Hello!",
timestamp = time.now():unix()
}})
Constants
HTTP Methods
http.METHOD.GET
http.METHOD.POST
http.METHOD.PUT
http.METHOD.DELETE
http.METHOD.PATCH
http.METHOD.HEAD
http.METHOD.OPTIONS
Status Codes
-- Success (2xx)
http.STATUS.OK -- 200
http.STATUS.CREATED -- 201
http.STATUS.ACCEPTED -- 202
http.STATUS.NO_CONTENT -- 204
http.STATUS.PARTIAL_CONTENT -- 206
-- Redirect (3xx)
http.STATUS.MOVED_PERMANENTLY -- 301
http.STATUS.FOUND -- 302
http.STATUS.SEE_OTHER -- 303
http.STATUS.NOT_MODIFIED -- 304
http.STATUS.TEMPORARY_REDIRECT -- 307
http.STATUS.PERMANENT_REDIRECT -- 308
-- Client Error (4xx)
http.STATUS.BAD_REQUEST -- 400
http.STATUS.UNAUTHORIZED -- 401
http.STATUS.PAYMENT_REQUIRED -- 402
http.STATUS.FORBIDDEN -- 403
http.STATUS.NOT_FOUND -- 404
http.STATUS.METHOD_NOT_ALLOWED -- 405
http.STATUS.NOT_ACCEPTABLE -- 406
http.STATUS.CONFLICT -- 409
http.STATUS.GONE -- 410
http.STATUS.UNPROCESSABLE -- 422
http.STATUS.TOO_MANY_REQUESTS -- 429
-- Server Error (5xx)
http.STATUS.INTERNAL_ERROR -- 500
http.STATUS.NOT_IMPLEMENTED -- 501
http.STATUS.BAD_GATEWAY -- 502
http.STATUS.SERVICE_UNAVAILABLE -- 503
http.STATUS.GATEWAY_TIMEOUT -- 504
http.STATUS.VERSION_NOT_SUPPORTED -- 505
Content Types
http.CONTENT.JSON -- "application/json"
http.CONTENT.FORM -- "application/x-www-form-urlencoded"
http.CONTENT.MULTIPART -- "multipart/form-data"
http.CONTENT.TEXT -- "text/plain"
http.CONTENT.STREAM -- "application/octet-stream"
Transfer Modes
http.TRANSFER.CHUNKED -- "chunked"
http.TRANSFER.SSE -- "sse"
Error Types
Module-specific error type constants for precise error handling.
http.ERROR.PARSE_FAILED -- Form/multipart parse error
http.ERROR.INVALID_STATE -- Invalid response state
http.ERROR.WRITE_FAILED -- Response write error
http.ERROR.STREAM_ERROR -- Body stream error
Errors
| Condition | Kind | Retryable |
|---|---|---|
| No HTTP context | errors.INTERNAL |
no |
| Body too large | errors.INVALID |
no |
| Read timeout | errors.INTERNAL |
no |
| Invalid JSON | errors.INVALID |
no |
| Not multipart | errors.INVALID |
no |
| Headers already sent | errors.INVALID |
no |
| Write failed | errors.INTERNAL |
no |
See Error Handling for working with errors.