Middleware
Middleware modifies the way a request is processed.
Since Lua is a very flexible language, there are lots of ways to implement middlewares.
We decided to take an advantage of Lua functions being a first-class values.
note
If you are familiar with this concept, feel free to go to the Full example at the bottom of the page.
Astra has several built-in middlewares. You can modify or extend middlewares directly in .astra/middleware.lua (after running astra export) or in your own files. We are opened to PRs.
For more details, see the middleware.lua and the middleware_example.lua.
Basic middleware
The following example shows the most basic middleware that changes the response headers.
local http = require("astra.lua.http")
local server = http.server:new()
local function sunny_day(request, response)
return "What a great sunny day!"
end
--- `on Leave:`
--- sets `"Content-Type": "text/html"` response header
local function html(next_handler)
return function(request, response, ctx)
result = next_handler(request, response, ctx)
response:set_header("Content-Type", "text/html")
return result
end
end
server:get("/sunny-day-plain-text", sunny_day)
server:get("/sunny-day-html", html(sunny_day))
server:run()
Context
When we want to pass data through middleware, we can use the third argument and treat it as a context table.
local datetime = require("astra.lua.datetime")
local server = http.server:new()
---@param ctx { datetime: DateTime }
local function favourite_day(_request, _response, ctx)
local today = string.format(
"%d/%d/%d",
ctx.datetime:get_day(),
ctx.datetime:get_month(),
ctx.datetime:get_year()
)
return "My favourite day is " .. today
end
--- `on Entry:`
--- Inserts `datetime.new()` into `ctx.datetime`
---
--- `Depends on:`
--- `ctx`
local function insert_datetime(next_handler)
return function(request, response, ctx)
ctx.datetime = datetime.new()
return next_handler(request, response, ctx)
end
end
--- `on Entry:`
--- Creates a new `ctx` table and passes it as a third argument into the `next_handler`
local function ctx(next_handler)
return function(request, response)
local ctx = {}
return next_handler(request, response, ctx)
end
end
--- `on Leave:`
--- sets `"Content-Type": "text/html"` response header
local function html(next_handler)
return function(request, response, ctx)
result = next_handler(request, response, ctx)
response:set_header("Content-Type", "text/html")
return result
end
end
server:get("/favourite-day", ctx(insert_datetime(html(favourite_day))))
server:run()
Chaining middlewares
To make it less tedious to compose middleware, we introduced the chain function, which combines all provided middleware into a single middleware.
note
Read more about why we can drop parenthesis while calling chain function here: Writing a DSL in Lua
local chain = http.middleware.chain
-- This will behave exactly the same as ctx(insert_datetime(html(favourite_day)))
server:get("/favourite-day", chain {ctx, insert_datetime, html} (favourite_day) )
-- We can create a common middlewares and reuse them
local composed_middleware = chain {ctx, insert_datetime, html}
server:get("/favourite-day-again", composed_middleware(favourite_day))
server:run()
Complex middleware
We can use Lua closures to create more complex middlewares.
This example shows how to create a file logger:
--- `on Entry:`
--- Logs request method and uri into the file
---@param file_handler file* A file handler opened with an append mode `io.open("filepath", "a")`
---@param flush_interval number? The number of log entries after which the file handler will be flushed
local function file_logger(file_handler, flush_interval)
local flush_interval = flush_interval or 1
local flush_countdown = flush_interval
return function(next_handler)
return function(request, response, ctx)
local str = string.format("[New Request %s] %s %s\n", os.date(), request:method(), request:uri())
file_handler:write(str)
flush_countdown = flush_countdown - 1
if flush_countdown == 0 then
file_handler:flush()
flush_countdown = flush_interval
end
return next_handler(request, response, ctx)
end
end
end
local file_handler, err = io.open("logs.txt", "a")
if not file_handler then error(err) end
local logger = file_logger(file_handler)
local common = chain { ctx, logger, html }
server:get("/sunny-day", common(sunny_day))
server:get("/normal-day", common(normal_day))
server:get("/favourite-day", chain { common, insert_datetime } (favourite_day))
server:run()
The logger we got from the file_logger is gonna be used in all routes we pass it as a middleware.
Full example
local http = require("astra.lua.http")
local datetime = require("astra.lua.datetime")
local server = http.server:new()
local chain = http.middleware.chain
local function sunny_day(_request, _response)
return "What a great sunny day!"
end
local function normal_day(_request, _response)
return "It's a normal day... I guess..."
end
---@param ctx { datetime: DateTime }
local function favourite_day(_request, _response, ctx)
local today = string.format(
"%d/%d/%d",
ctx.datetime:get_day(),
ctx.datetime:get_month(),
ctx.datetime:get_year()
)
return "My favourite day is " .. today
end
--- `on Entry:`
--- Creates a new `ctx` table and passes it as a third argument into the `next_handler`
local function ctx(next_handler)
return function(request, response)
local ctx = {}
return next_handler(request, response, ctx)
end
end
--- `on Entry:`
--- Inserts `datetime.new()` into `ctx.datetime`
---
--- `Depends on:`
--- `ctx`
local function insert_datetime(next_handler)
return function(request, response, ctx)
ctx.datetime = datetime.new()
return next_handler(request, response, ctx)
end
end
--- `on Leave:`
--- sets `"Content-Type": "text/html"` response header
local function html(next_handler)
return function(request, response, ctx)
result = next_handler(request, response, ctx)
response:set_header("Content-Type", "text/html")
return result
end
end
--- `on Entry:`
--- Logs request method and uri into the file
---@param file_handler file* A file handler opened with an append mode `io.open("filepath", "a")`
---@param flush_interval number? The number of log entries after which the file handler will be flushed
local function file_logger(file_handler, flush_interval)
local flush_interval = flush_interval or 1
local flush_countdown = flush_interval
return function(next_handler)
return function(request, response, ctx)
local str = string.format("[New Request %s] %s %s\n", os.date(), request:method(), request:uri())
file_handler:write(str)
flush_countdown = flush_countdown - 1
if flush_countdown == 0 then
file_handler:flush()
flush_countdown = flush_interval
end
return next_handler(request, response, ctx)
end
end
end
local file_handler, err = io.open("logs.txt", "a")
if not file_handler then error(err) end
local logger = file_logger(file_handler)
server:get("/sunny-day", logger(html(sunny_day)))
server:get("/normal-day", chain { logger, html } (normal_day))
server:get("/favourite-day", chain { ctx, logger, insert_datetime, html } (favourite_day))
server:run()