Envoy Lua filters let you inject custom logic into the request/response path without the overhead of a full-service sidecar.
Consider a scenario where you need to add a custom header to all incoming requests before they hit your upstream services.
-- envoy/filters/http/lua/config/custom_header.lua
function envoy_on_request(request_handle)
request_handle:logInfo("Adding custom header to request.")
request_handle:headers():add("x-custom-envoy-header", "processed-by-lua")
end
This simple Lua script, when configured as an Envoy HTTP filter, will append x-custom-envoy-header: processed-by-envoy to every request it processes.
To enable this, you’d configure it in your Envoy http_filter section:
# envoy.yaml
admin:
access_log_path: /tmp/envoy.log
address:
socket_address:
address: 127.0.0.1
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
default_source_code:
inline_string: |
function envoy_on_request(request_handle)
request_handle:logInfo("Adding custom header to request.")
request_handle:headers():add("x-custom-envoy-header", "processed-by-lua")
end
The envoy_on_request function is the entry point for request-side logic. Envoy calls this function for each incoming request. The request_handle object is your primary interface to the request and response. You can access headers, body, query parameters, and even modify them.
The request_handle:headers() method returns a table-like object representing the request headers. The add(key, value) method appends a new header. If a header with the same key already exists, it will be added as a distinct header. To replace a header, you’d typically use remove followed by add.
Here’s a more complex example: conditionally modifying a header based on the request path.
-- envoy/filters/http/lua/config/conditional_header.lua
function envoy_on_request(request_handle)
local path = request_handle:headers():get(":path")
if path and string.match(path, "^/api/v1/users") then
request_handle:logInfo("Adding specific header for user API.")
request_handle:headers():add("x-user-api-version", "v1")
end
end
This filter checks if the request path starts with /api/v1/users. If it does, it adds the x-user-api-version: v1 header.
You can also intercept responses using the envoy_on_response function:
-- envoy/filters/http/lua/config/response_log.lua
function envoy_on_response(request_handle)
local status = request_handle:headers():get(":status")
request_handle:logInfo("Request to " .. request_handle:headers():get(":path") .. " returned status: " .. (status or "unknown"))
end
This filter logs the request path and its corresponding HTTP status code.
The request_handle object provides access to other useful methods. For instance, request_handle:body() can retrieve the request body as a string, and request_handle:body_length() gives its size. You can also modify the body, though this is more complex and often involves stream-based manipulation for efficiency.
Consider a filter that needs to rewrite a query parameter.
-- envoy/filters/http/lua/config/query_rewrite.lua
function envoy_on_request(request_handle)
local uri = request_handle:request_uri()
local new_uri = string.gsub(uri, "search=old", "search=new")
if new_uri ~= uri then
request_handle:logInfo("Rewriting query parameter in URI.")
request_handle:request_uri(new_uri)
end
end
This filter uses string.gsub to find and replace the query parameter search=old with search=new in the request URI. The request_handle:request_uri(new_uri) call updates the URI.
The Lua environment within Envoy is intentionally kept lightweight. It uses a standard Lua 5.1 interpreter. You can load external Lua modules using require, but they must be available in the path Envoy is configured to search. This is typically done by placing your Lua scripts in a directory and configuring Envoy to include that directory in its Lua path.
# envoy.yaml snippet for Lua path
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
source_code:
filename: /etc/envoy/lua/my_filter.lua
lua_extension_module_paths:
- /etc/envoy/lua/modules
In the my_filter.lua file, you could then require("my_utils") if my_utils.lua is in /etc/envoy/lua/modules.
The request_handle object also exposes logInfo, logWarn, logErr for logging, and getMetadata / setMetadata to interact with metadata attached to the request. Metadata is a way to pass arbitrary key-value data across different filters.
One subtlety often missed is how Envoy handles asynchronous operations within Lua. While the provided examples are synchronous, Lua filters can initiate asynchronous operations (like making an HTTP call to another service via request_handle:httpCall). When an asynchronous operation is initiated, the Lua script yields control back to Envoy. Envoy then continues processing other requests or tasks. When the asynchronous operation completes, Envoy resumes the Lua script at the point it yielded, typically within a callback function. This non-blocking nature is crucial for maintaining Envoy’s high performance. If your Lua code performs blocking I/O directly, you’ll grind Envoy to a halt.
The next step is usually integrating with other Envoy filters, like the rate limiting filter, to apply policies based on custom headers you’ve added.