The HTTPServer interface provides a comprehensive HTTP service that runs in the background of the calling application. It supports static file serving, directory indexing, security headers, rate limiting, and comprehensive request handling.
The server is designed for security and reliability, implementing features such as path validation, directory traversal protection, rate limiting, and proper error handling.
It can be loaded with the line:
local httpServer = require 'net/httpserver'
server = httpServer.start(Options)
Creates a new HTTP server instance with the specified configuration options. The server will start immediately before returning unless an error is raised.
This example creates a basic HTTP server serving files from the parasol:docs/html/
folder on port 7070:
local httpServer = require 'net/httpserver'
local server = httpServer.start({
port = 7070,
folder = 'parasol:docs/html/'
})
Valid options to use when defining the server configuration are as follows:
Option | Description | Default |
---|---|---|
folder | REQUIRED. The folder containing HTML pages and static files to serve | none |
port | The port number to listen on | 8080 |
bind | Bind to specific IP address (e.g., '127.0.0.1' for localhost only) | all interfaces |
verbose | Enable verbose logging (true/false) | false |
timeout | Request timeout in seconds (1-300) | 30 |
autoIndex | Enable automatic directory listing (true/false) | true |
maxRequestSize | Maximum request size in bytes | 65536 |
fileChunkSize | File streaming chunk size in bytes | 65536 |
rateLimit | Maximum requests per minute per IP (0-1000, 0 = disabled) | 100 |
rateLimitWindow | Rate limit window in seconds | 60 |
enableCSP | Enable Content-Security-Policy headers (true/false) | true |
routes | Array of route configurations for declarative routing setup | none |
middleware | Array of global middleware functions to apply to all routes | none |
server.stop()
Stops the HTTP server and releases all resources. The server socket is closed and garbage collection is triggered to clean up resources.
server.newMiddleware(MiddlewareFunction)
Adds a global middleware function to the server that will be executed for all routes. Middleware functions are executed in the order they are added.
server.newMiddleware(function(req, res, next)
print('Processing request: ' .. req.path)
next() -- Continue to next middleware or route handler
end)
The server provides built-in middleware factory functions accessible via server.middleware
:
Creates CORS (Cross-Origin Resource Sharing) middleware with configurable options:
local corsMiddleware = server.middleware.cors({
origin = 'https://myapp.com', -- Allowed origins (default: '*')
methods = 'GET, POST, PUT, DELETE', -- Allowed methods
headers = 'Content-Type, Authorization', -- Allowed headers
credentials = true, -- Allow credentials
maxAge = 3600 -- Preflight cache time in seconds
})
server.newMiddleware(corsMiddleware)
Creates request logging middleware that logs request details and response times:
local loggingMiddleware = server.middleware.logging()
server.newMiddleware(loggingMiddleware)
Creates authentication middleware that validates Bearer tokens:
local authMiddleware = server.middleware.auth({
header = 'authorization', -- Header name (default: 'authorization')
prefix = 'Bearer ' -- Token prefix (default: 'Bearer ')
})
server.newMiddleware(authMiddleware)
The HTTP server includes a comprehensive middleware system that allows you to execute functions before and after route handlers. Middleware functions follow the (request, response, next)
signature pattern, similar to Express.js.
Middleware executes in the following order:
All middleware functions must follow this signature:
function middlewareFunction(request, response, next)
-- Middleware logic here
-- Call next() to continue to the next middleware or route handler
next()
-- Or handle the request and don't call next() to stop the chain
-- response.json({ error = 'Blocked by middleware' })
end
Global middleware can be configured during server creation or added dynamically:
-- Configure during server creation
local server = httpServer.start({
port = 8080,
folder = './public/',
middleware = {
function(req, res, next)
print('Request received: ' .. req.method .. ' ' .. req.path)
next()
end,
function(req, res, next)
req.timestamp = mSys.PreciseTime()
next()
end
}
})
-- Add middleware dynamically
server.newMiddleware(function(req, res, next)
if req.method == 'POST' and not req.headers['content-type'] then
res.status(400).json({ error = 'Content-Type header required' })
return
end
next()
end)
Middleware can be applied to specific routes via the middleware
property in route configurations:
local server = httpServer.start({
port = 8080,
folder = './public/',
routes = {
{
pattern = '/api/protected',
method = 'GET',
handler = function(req, res)
res.json({ message = 'Access granted', user = req.auth.user })
end,
middleware = {
-- Authentication middleware for this route only
function(req, res, next)
local token = req.headers.authorization
if not token or not token:find('Bearer ') then
res.status(401).json({ error = 'Authentication required' })
return
end
req.auth = { user = 'authenticated_user', token = token }
next()
end,
-- Logging middleware for this route only
function(req, res, next)
print('Protected route accessed by: ' .. req.auth.user)
next()
end
}
}
}
})
The newRoute()
method also accepts an optional middleware parameter:
server.newRoute({
method = 'POST',
pattern = '/api/upload',
handler = function(req, res)
res.json({ message = 'File uploaded successfully' })
end,
middleware = {
-- Route-specific middleware
function(req, res, next)
if not req.headers['content-length'] then
res.status(400).json({ error = 'Content-Length header required' })
return
end
next()
end
})
Middleware functions are executed within a pcall()
. If a middleware function throws an error, the request will be terminated with a 500 Internal Server Error response:
server.newMiddleware(function(req, res, next)
if someCondition then
error('Something went wrong!') -- Will send 500 error to client
end
next()
end)
local function timingMiddleware(req, res, next)
local startTime = mSys.PreciseTime()
-- Override response methods to log timing
local originalJson = res.json
res.json = function(data)
local duration = (mSys.PreciseTime() - startTime) / 1000
print('Request completed in ' .. string.format('%.2f', duration) .. 'ms')
return originalJson(data)
end
next()
end
local function validateJsonMiddleware(req, res, next)
if req.method == 'POST' or req.method == 'PUT' then
if not req.parsedBody or type(req.parsedBody) ~= 'table' then
res.status(400).json({ error = 'Valid JSON body required' })
return
end
end
next()
end
local function apiVersionMiddleware(req, res, next)
local version = req.headers['api-version'] or '1.0'
req.apiVersion = version
if version ~= '1.0' and version ~= '2.0' then
res.status(400).json({ error = 'Unsupported API version: ' .. version })
return
end
next()
end
The HTTP server includes a comprehensive routing system that supports regex-based URL pattern matching with parameter extraction and callback functions. Routes are registered per-server instance and support multiple HTTP methods.
There are two ways to register routes: declaratively via server configuration or programmatically using the newRoute()
method.
Routes can be configured during server creation using the routes
option. This is the most concise approach for setting up multiple routes:
local server = httpServer.start({
port = 8080,
folder = './public/',
routes = {
{ pattern = '/api/users/:id', method = 'GET', handler = function(req, res)
local userId = req.params.id
res.json({ userId = userId, message = 'User retrieved' })
end },
{ pattern = '/api/users', method = 'POST', handler = function(req, res)
local userData = req.parsedBody
res.json({ message = 'User created', data = userData })
end },
{ pattern = '/api/users/:id', method = 'PUT', handler = function(req, res)
local userId = req.params.id
local userData = req.parsedBody
res.json({ userId = userId, message = 'User updated', data = userData })
end },
{ pattern = '/api/users/:id', method = 'DELETE', handler = function(req, res)
local userId = req.params.id
res.status(204).send('')
end }
}
})
Route Configuration Format:
Each route in the routes
array must contain:
pattern
- URL pattern with optional parameters (:param
) or wildcards (*
)method
- HTTP method as string (case-insensitive)handler
- Function to handle the route with signature function(req, res)
middleware
- (Optional) Array of middleware functions specific to this routevalidateHeader
- (Optional) Function to validate request headers before processing bodyserver.newRoute(Method, Pattern, Callback, Middleware)
Registers a route with the specified HTTP method, URL pattern, callback function, and optional route-specific middleware. Use this method for dynamic route registration after server creation:
-- Register routes after server creation
server.newRoute({
method = 'GET',
pattern = '/api/status',
handler = function(req, res)
res.json({ status = 'OK', timestamp = mSys.PreciseTime() })
end
})
server.newRoute({
method = 'POST',
pattern = '/api/upload',
handler = function(req, res)
-- Handle file upload
res.json({ message = 'Upload successful' })
end
})
Supported HTTP methods are GET
, POST
, PUT
, DELETE
, PATCH
, HEAD
, OPTIONS
The routing system supports several pattern types for flexible URL matching:
server.newRoute({
method = 'GET',
pattern = '/api/status',
handler = function(req, res) res.json({ status = 'OK' }) end
})
Use :parameter
syntax to capture URL segments as named parameters:
server.newRoute({
method = 'GET',
pattern = '/api/users/:id',
handler = function(req, res)
res.json({ userId = req.params.id })
end
})
server.newRoute({
method = 'GET',
pattern = '/api/users/:id/posts/:postId',
handler = function(req, res)
res.json({ userId = req.params.id, postId = req.params.postId })
end
})
Use *
to match any remaining path segments:
server.newRoute({
method = 'GET',
pattern = '/api/files/*',
handler = function(req, res)
local fullPath = req.path -- Contains the complete matched path
res.json({ path = fullPath, message = 'File route matched' })
end
})
Route callbacks receive an enhanced request object with the following properties:
Property | Description |
---|---|
method | HTTP method (GET, POST, etc.) |
path | Full request path |
fullPath | Complete URL path including query string |
query | Query string parameters as table |
params | Route parameters extracted from URL pattern |
headers | Request headers as table (lowercase keys) |
cookies | Parsed cookies as table |
body | Raw request body string (for POST/PUT/PATCH requests) |
parsedBody | Parsed request body (JSON or form data) |
ip | Client IP address string (available in middleware and route handlers) |
Route callbacks receive a response object with chainable methods:
res.json(Data)
Sends a JSON response with appropriate Content-Type headers.
res.json({ message = 'Success', data = userData })
res.send(Content, ContentType)
Sends a response with custom content and optional content type.
res.send('Hello World', 'text/plain')
res.status(StatusCode, StatusText)
Sets the HTTP status code and optional status text for the response.
res.status(404).json({ error = 'Not found' })
res.status(201, 'Created').json({ message = 'User created' })
res.header(Name, Value)
Sets a response header.
res.header('Cache-Control', 'no-cache').json({ data = result })
res.cookie(Name, Value, Options)
Sets a cookie with optional configuration.
res.cookie('sessionId', '12345', {
maxAge = 3600,
httpOnly = true,
secure = true
})
res.redirect(URL, StatusCode)
Sends a redirect response (default 302).
res.redirect('/login')
res.redirect('https://example.com', 301)
Routes support optional header validation through the validateHeader
callback function. This allows early validation of request headers before the request body is processed, providing better performance and security for routes that require specific headers.
The validateHeader
function receives the request headers as a table and can return:
true
or nil
- Headers are valid, continue processingfalse
- Headers are invalid, send 400 Bad Request responsetable
- Custom error response with status
, statusText
, message
, and details
fields{
pattern = '/api/protected',
method = 'GET',
validateHeader = function(headers)
local auth = headers['authorization']
if not auth then
return {
status = 401,
statusText = 'Unauthorized',
message = 'Authentication required',
details = 'Missing Authorization header'
}
end
local token = auth:match('Bearer%s+(.+)')
if not token or token ~= 'valid-token-123' then
return false -- Simple 400 Bad Request
end
return true -- Valid, continue processing
end,
handler = function(req, res)
res.json({ message = 'Access granted', user = 'authenticated' })
end
}
Routes are matched in the order they are registered. More specific routes should be registered before more general ones:
-- Register specific routes first
server.newRoute({ method='GET', pattern='/api/users/admin', handler=adminHandler })
server.newRoute({ method='GET', pattern='/api/users/:id', handler=userHandler })
-- General routes last
server.newRoute({ method='GET', pattern='/api/*', handler=catchAllHandler })
The HTTP server implements comprehensive security measures:
../
sequencesThe server provides comprehensive error handling with styled HTML error pages for:
local server = httpServer.start({
port = 8080,
folder = 'parasol:docs/html/',
verbose = true
})
local httpServer = require 'net/httpserver'
local server = httpServer.start({
port = 3000,
folder = './public/',
verbose = true,
routes = {
{ pattern = '/api/status', method = 'GET', handler = function(req, res)
res.json({ status = 'OK', timestamp = mSys.PreciseTime() })
end },
{ pattern = '/api/users/:id', method = 'GET', handler = function(req, res)
local userId = req.params.id
res.json({ userId = userId, message = 'User retrieved' })
end },
{ pattern = '/api/users', method = 'POST', handler = function(req, res)
local userData = req.parsedBody
res.json({ message = 'User created', data = userData })
end }
}
})
local server = httpServer.start({
port = 3000,
folder = '/path/to/secure/files/',
bind = '127.0.0.1',
autoIndex = false,
rateLimit = 50,
timeout = 15
})
local server = httpServer.start({
port = 80,
folder = '/var/www/html/',
maxRequestSize = 131072, -- 128KB
fileChunkSize = 131072, -- 128KB chunks
rateLimit = 200,
rateLimitWindow = 30,
enableCSP = false -- Disable if custom headers needed
})
local httpServer = require 'net/httpserver'
local server = httpServer.start({
port = 8080,
folder = './static/',
verbose = true,
routes = {
-- RESTful user management API
{ pattern = '/api/users', method = 'GET', handler = function(req, res)
res.json({ users = getUserList() })
end },
{ pattern = '/api/users/:id', method = 'GET', handler = function(req, res)
local user = getUser(req.params.id)
if user then
res.json(user)
else
res.status(404).json({ error = 'User not found' })
end
end },
{ pattern = '/api/users', method = 'POST', handler = function(req, res)
local newUser = createUser(req.parsedBody)
res.status(201).json(newUser)
end },
{ pattern = '/api/users/:id', method = 'PUT', handler = function(req, res)
local updatedUser = updateUser(req.params.id, req.parsedBody)
res.json(updatedUser)
end },
{ pattern = '/api/users/:id', method = 'DELETE', handler = function(req, res)
(req.params.id)
deleteUserres.status(204).send('')
end },
-- File serving with wildcard routes
{ pattern = '/files/*', method = 'GET', handler = function(req, res)
local filePath = req.path:sub(8) -- Remove '/files/' prefix
res.send('Serving file: ' .. filePath)
end }
}
})