Integrating Lua Scripts into OpenResty: A Comprehensive Guide
Introduction to Lua Integration in OpenResty
OpenResty provides multiple mechanisms for embedding Lua code directly into the Nginx configuration. This article explores the various methods available for incorporating Lua logic into your OpenResty applications.
Methods for Embedding Lua in OpenResty
Direct String Execution with content_by_lua
The most straightforward approach uses inline Lua code as a string parameter:
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type text/html;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
location /hello {
content_by_lua 'ngx.say("greetings from lua")';
}
}
}
While simple, this approach becomes unwieldy for complex logic due to string escaping requirements.
Code Block Execution with content_by_lua_block
For more readable inline Lua code, the block syntax eliminates string quoting issues:
location /demo {
content_by_lua_block {
local message = "execution phase test"
ngx.say("Result: " .. message)
}
}
External File Loading with content_by_lua_file
For substantial Lua codebases, external files provide better maintainability:
location /script {
content_by_lua_file /opt/app/lua handlers/main.lua;
}
Create the Lua file at the specified path:
-- handlers/main.lua
ngx.say("external script loaded successfully")
Live Reload Configuration
By default, Nginx caches compiled Lua code. During development, disable caching to reflect changes immediately:
http {
lua_code_cache off;
server {
listen 8080;
# ... configuration
}
}
Important: Always enable lua_code_cache on in production environments. Disabling caching significantly impacts performance.
Lua Module Path Configuration
The lua_package_path directive specifies directories where OpenResty searches for Lua modules:
http {
lua_package_path "/opt/app/lua/?.lua;/lib/resty/?.lua;;";
}
The double semicolon ;; represents the default module path.
Core Output Operations
Response Header and Body Management
location /output-demo {
content_by_lua_block {
-- Set custom response headers
ngx.header.content_type = "application/json"
ngx.header.x_request_id = "req-abc123"
-- Output content (with newline)
ngx.say("First line")
ngx.say("Second line")
-- Output content (without newline)
ngx.print("No newline here")
ngx.print(" - continuing")
-- Terminate with specific status
return ngx.exit(ngx.HTTP_OK)
}
}
Key functions:
ngx.header: Manipulate response headersngx.print(): Output response bodyngx.say(): Output response body with trailing newlinengx.exit(): Terminate request with specified status code
Accessing Nginx Variables
Built-in Nginx Variables
OpenResty exposes numerous Nginx variables through ngx.var:
| Variable | Description |
|---|---|
$remote_addr |
Client IP address |
$remote_port |
Client port number |
$request_method |
HTTP method (GET, POST, etc.) |
$query_string |
URL query parameters |
$uri |
Request URI path |
$request_uri |
Full request URI with parameters |
$http_user_agent |
Client browser identification |
$http_cookie |
Cookie data |
$server_addr |
Server IP address |
$server_port |
Server port number |
Retrieving Variables via ngx.var
location /variable-test {
set $custom_param "100";
content_by_lua_block {
-- Parse query parameter 'value'
local incoming = tonumber(ngx.var.arg_value) or 0
-- Access custom Nginx variable
local configured = tonumber(ngx.var.custom_param) or 0
local result = incoming + configured
ngx.say("Calculated: " .. result)
-- Access server information
ngx.say("Server Address: " .. ngx.var.server_addr)
}
}
Extracting Captured Groups from Regex Locations
When using regex in location blocks, capture groups become accessible:
location ~ ^/api/user/(\d+)/profile$ {
content_by_lua_block {
local user_id = ngx.var[1]
ngx.say("User ID extracted: " .. user_id)
}
}
Accessing /api/user/5421/profile would yield "User ID extracted: 5421".
Request Processing APIs
Reading Request Headers
-- main.lua
local request_headers = ngx.req.get_headers()
ngx.say("=== Request Headers ===", "<br/>")
ngx.say("Host: ", request_headers["Host"], "<br/>")
ngx.say("User-Agent: ", request_headers.user_agent, "<br/>")
-- Iterate through all headers
for header_name, header_value in pairs(request_headers) do
if type(header_value) == "table" then
ngx.say(header_name .. ": " .. table.concat(header_value, ", "), "<br/>")
else
ngx.say(header_name .. ": " .. header_value, "<br/>")
end
end
Processing Query Parameters
-- Extract GET parameters
local uri_params = ngx.req.get_uri_args()
ngx.say("=== URI Parameters ===", "<br/>")
for param_key, param_value in pairs(uri_params) do
if type(param_value) == "table" then
ngx.say(param_key .. ": [" .. table.concat(param_value, ", ") .. "]", "<br/>")
else
ngx.say(param_key .. ": " .. param_value, "<br/>")
end
end
Processing POST Request Body
-- Must read body before accessing POST data
ngx.req.read_body()
local post_data = ngx.req.get_post_args()
ngx.say("=== POST Data ===", "<br/>")
for field_name, field_value in pairs(post_data) do
if type(field_value) == "table" then
ngx.say(field_name .. ": [" .. table.concat(field_value, ", ") .. "]", "<br/>")
else
ngx.say(field_name .. ": " .. field_value, "<br/>")
end
end
-- Alternatively, get raw body string
local raw_body = ngx.req.get_body_data()
Comparison: ngx.var.arg vs ngx.req.get_uri_args
Both methods retrieve URL parameters, but with key differences:
-- URL: /compare?item=first&item=second&item=third
-- Returns first value only
local single = ngx.var.arg_item -- "first"
-- Returns table containing all values
local multiple = ngx.req.get_uri_args()["item"] -- {"first", "second", "third"}
Encoding and Decoding Utilities
URI Encoding/Decoding
local original_uri = ngx.var.request_uri
ngx.say("Original: ", original_uri, "<br/>")
local encoded = ngx.escape_uri(original_uri)
ngx.say("Encoded: ", encoded, "<br/>")
local decoded = ngx.unescape_uri(encoded)
ngx.say("Decoded: ", decoded, "<br/>")
Parameter Table Encoding
local request_uri = ngx.var.request_uri
local question_index = string.find(request_uri, '?')
if question_index then
local query_string = string.sub(request_uri, question_index + 1)
local decoded_params = ngx.decode_args(query_string)
for key, value in pairs(decoded_params) do
ngx.say(key .. " = " .. value, "<br/>")
end
-- Modify and re-encode parameters
if decoded_params.userId then
local modified_id = tonumber(decoded_params.userId) + 5000
decoded_params.userId = modified_id
ngx.say("Modified: " .. ngx.encode_args(decoded_params), "<br/>")
end
end
Base64 Operasions
local input = "sensitive data here"
local encoded = ngx.encode_base64(input)
local decoded = ngx.decode_base64(encoded)
Cryptographic Functions
-- MD5 hash (hexadecimal output)
local hash_result = ngx.md5("password123")
ngx.say("MD5: ", hash_result, "<br/>")
-- Binary MD5
local bin_hash = ngx.md5_bin("password123")
-- SHA1 binary
local sha1_result = ngx.sha1_bin("content")
Time APIs
OpenResty provides high-performance time retrieval functions that use Nginx's internal cache:
location /time-demo {
content_by_lua_block {
-- Second-level precision timestamp
local current_seconds = ngx.time()
ngx.say("Unix timestamp: ", current_seconds, "<br/>")
-- Millisecond-level precision
local current_ms = ngx.now()
ngx.say("Precise timestamp: ", current_ms, "<br/>")
-- Demonstrate cached time behavior
local start = ngx.now()
-- Simulate processing delay
for i = 1, 500000 do
local _ = math.random()
local finish = ngx.now()
ngx.say("Start: ", start, " | Finish: ", finish, "<br/>")
-- Force cache refresh
ngx.update_time()
local refreshed = ngx.now()
ngx.say("After refresh: ", refreshed)
}
}
Note: ngx.now() returns cached timestamps for performance. Use ngx.update_time() to refresh the cache when needed.
Regular Expression Operations
Pattern Matching with ngx.re.match
local test_string = "Product ID: 98765"
-- Simple numeric extraction
local match_result, match_error = ngx.re.match(test_string, "[0-9]+")
if match_result then
ngx.say("Full match: ", match_result[0], "<br/>")
else
if match_error then
ngx.log(ngx.ERR, "Match error: ", match_error)
end
ngx.say("No match found")
end
-- Using capture groups
local capture_result = ngx.re.match("Price: $199.99", "(\$\\d+\\.\\d+)")
if capture_result then
ngx.say("Complete match: ", capture_result[0], "<br/>") -- $199.99
ngx.say("Group 1: ", capture_result[1], "<br/>") -- $199.99
end
Other regex functions:
ngx.re.sub(): Replace first occurrencengx.re.gsub(): Replace all occurrencesngx.re.find(): Find position without extractionngx.re.gmatch(): Iterate over matches
Logging System
Writing to Error Log
error_log logs/application.log info;
location /log-demo {
content_by_lua_block {
local numeric_value = 42
local text_value = "configuration"
local nil_object = nil
-- Different log levels
ngx.log(ngx.ERR, "Error occurred with value: ", numeric_value)
ngx.log(ngx.WARN, "Warning: unexpected input")
ngx.log(ngx.INFO, "Processing: ", text_value)
-- Using print (maps to NOTICE level)
print("Debug output via print function")
ngx.log(ngx.ERR, "Nil reference: ", tostring(nil_object))
return ngx.exit(ngx.HTTP_OK)
}
}
Log level hierarchy (ascending severity):
- DEBUG < INFO < NOTICE < WARN < ERR < CRIT < ALERT < EMERG
Request Redirection
Performing Redirects
location = /original-path {
content_by_lua_block {
ngx.say("This is the target location")
}
}
location = /redirect-source {
rewrite_by_lua_block {
return ngx.redirect("/original-path")
end
}
-- External redirect example
location = /external {
rewrite_by_lua_block {
return ngx.redirect("https://example.com/new-location", ngx.HTTP_MOVED_TEMPORARILY)
end
}
Cross-Phase Data Sharing with ngx.ctx
Request-Scoped Context Table
The ngx.ctx table maintains data throughout a single request's lifecycle across different Nginx phases:
location /context-demo {
set $stage_marker "initial";
rewrite_by_lua_block {
ngx.ctx.request_data = {
stage = "rewrite",
sequence = 1,
tracking_id = "req-" .. ngx.var.request_id
}
ngx.ctx.compute_value = 100
}
access_by_lua_block {
-- Access data from rewrite phase
ngx.ctx.compute_value = ngx.ctx.compute_value + 50
ngx.ctx.request_data.stage = "access"
ngx.ctx.request_data.sequence = 2
}
content_by_lua_block {
ngx.say("Stage: ", ngx.ctx.request_data.stage, "<br/>")
ngx.say("Sequence: ", ngx.ctx.request_data.sequence, "<br/>")
ngx.say("Tracking: ", ngx.ctx.request_data.tracking_id, "<br/>")
ngx.say("Computed: ", ngx.ctx.compute_value)
}
}
Subrequest Context Isolation
Each request and subrequest maintains its own ngx.ctx table:
location = /sub-context {
content_by_lua_block {
ngx.say("Sub - Before: ", ngx.ctx.shared_value)
ngx.ctx.shared_value = 999
ngx.say("Sub - After: ", ngx.ctx.shared_value)
}
}
location = /main-context {
content_by_lua_block {
ngx.ctx.shared_value = 111
ngx.say("Main - Before subrequest: ", ngx.ctx.shared_value)
local subresponse = ngx.location.capture("/sub-context")
ngx.print(subresponse.body)
ngx.say("Main - After subrequest: ", ngx.ctx.shared_value)
end
}
Output demonstrates isolation:
Main - Before subrequest: 111
Sub - Before: nil
Sub - After: 999
Main - After subrequest: 111
Performance Note: ngx.ctx uses metatable operations, making it slower than passing data through function arguments. Use judiciously in performance-critical paths.
Additional Nginx Lua APIs Reference
Core Directive Aliases
| Directive Alias | Function |
|---|---|
| ngx.arg[n] | Access directive parameters |
| ngx.var.VARIABLE_NAME | Reference Nginx variables |
| ngx.ctx | Request-scoped context table |
| ngx.header.HEADER_NAME | Manipulate response headers |
| ngx.status | Get/set response status code |
Response Handling
ngx.send_headers() -- Explicitly send headers
ngx.headers_sent -- Check if headers sent
ngx.flush(true) -- Flush output buffer
ngx.eof() -- Signal end of output
Request Information
local http_version = ngx.req.http_version() -- e.g., 1.1
local method = ngx.req.get_method() -- GET, POST, etc.
local raw_headers = ngx.req.raw_header() -- Full header string
local start_time = ngx.req.start_time() -- Request start timestamp
URL Manipulation
ngx.req.set_uri("/new/path") -- Rewrite URI
ngx.req.set_uri_args("a=1&b=2") -- Set query string
ngx.req.set_method(ngx.HTTP_POST) -- Override method
Hash Functions
local crc = ngx.crc32_short("data") -- CRC32 hash
local hmac = ngx.hmac_sha1("key", "data") -- HMAC-SHA1
Date/Time Formattting
local current_date = ngx.today() -- YYYY-MM-DD
local timestamp = ngx.time() -- Unix timestamp
local formatted = ngx.cookie_time(os.time()) -- Cookie-compatible format
local http_date = ngx.http_time(os.time()) -- HTTP header format
local parsed = ngx.parse_http_time("Thu, 01 Jan 1970") -- Parse HTTP date
SQL Escaping
local safe = ngx.quote_sql_str("O'Brien") -- Escapes: 'O\'Brien'
Worker Process Information
local debug_mode = ngx.config.debug -- Boolean
local install_prefix = ngx.config.prefix() -- Installation path
local nginx_version = ngx.config.nginx_version -- Version number
local lua_version = ngx.config.ngx_lua_version -- Lua module version
local worker_pid = ngx.worker.pid() -- Current worker PID
local is_exiting = ngx.worker.exiting() -- Shutdown status
HTTP Status Code Constants
ngx.HTTP_OK -- 200
ngx.HTTP_CREATED -- 201
ngx.HTTP_MOVED_PERMANENTLY -- 301
ngx.HTTP_MOVED_TEMPORARILY -- 302
ngx.HTTP_BAD_REQUEST -- 400
ngx.HTTP_UNAUTHORIZED -- 401
ngx.HTTP_FORBIDDEN -- 403
ngx.HTTP_NOT_FOUND -- 404
ngx.HTTP_INTERNAL_SERVER_ERROR -- 500
ngx.HTTP_SERVICE_UNAVAILABLE -- 503
Conclusion
OpenResty's Lua integration provides a powerful framework for extending Nginx with dynamic logic. The key components covered include inline and file-based Lua execution, variable access patterns, request processing APIs, encoding utilities, and cross-phase data sharing through ngx.ctx. These fundamentals enable the development of sophisticated gateway applications, API proxies, and request processing pipelines within the Nginx ecosystem.