Fluid HTTPServer API

HTTP Server

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'

Functions

httpServer.start()

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 Methods

server.stop()

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()

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)

server.middleware

The server provides built-in middleware factory functions accessible via server.middleware:

server.middleware.cors()

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)

server.middleware.logging()

Creates request logging middleware that logs request details and response times:

local loggingMiddleware = server.middleware.logging()
server.newMiddleware(loggingMiddleware)

server.middleware.auth()

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)

Middleware System

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 Execution Order

Middleware executes in the following order:

  1. Global middleware (applied to all routes) in registration order
  2. Route-specific middleware (defined per route) in definition order
  3. Route handler (the actual route callback)

Middleware Function Signature

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 Configuration

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)

Route-Specific Middleware

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
         }
      }
   }
})

Dynamic Route Registration with Middleware

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 Error Handling

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)

Common Middleware Patterns

Request Timing

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

Request Validation

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

API Versioning

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

Routing System

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.

Route Registration Methods

There are two ways to register routes: declaratively via server configuration or programmatically using the newRoute() method.

Declarative Route Configuration

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 route
  • validateHeader - (Optional) Function to validate request headers before processing body

server.newRoute()

server.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

Route Patterns

The routing system supports several pattern types for flexible URL matching:

Exact Matches

server.newRoute({ 
   method = 'GET', 
   pattern = '/api/status', 
   handler = function(req, res) res.json({ status = 'OK' }) end
})

Parameter Routes

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
})

Wildcard Routes

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
})

Request Object

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)

Response Object

Route callbacks receive a response object with chainable methods:

res.json()

res.json(Data)

Sends a JSON response with appropriate Content-Type headers.

res.json({ message = 'Success', data = userData })

res.send()

res.send(Content, ContentType)

Sends a response with custom content and optional content type.

res.send('Hello World', 'text/plain')

res.status()

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()

res.header(Name, Value)

Sets a response header.

res.header('Cache-Control', 'no-cache').json({ data = result })

res.cookie()

res.cookie(Name, Value, Options)

Sets a cookie with optional configuration.

res.cookie('sessionId', '12345', { 
   maxAge = 3600, 
   httpOnly = true, 
   secure = true 
})

res.redirect()

res.redirect(URL, StatusCode)

Sends a redirect response (default 302).

res.redirect('/login')
res.redirect('https://example.com', 301)

Header Validation

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.

validateHeader Function

The validateHeader function receives the request headers as a table and can return:

  • true or nil - Headers are valid, continue processing
  • false - Headers are invalid, send 400 Bad Request response
  • table - 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
}

Route Priority

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 })

Features

Security

The HTTP server implements comprehensive security measures:

  • Path Validation: Prevents directory traversal attacks using ../ sequences
  • File Type Restrictions: Blocks access to executables, configuration files, and hidden files
  • Rate Limiting: Configurable per-IP request limits to prevent DoS attacks
  • Security Headers: Automatic addition of CSP, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy headers
  • Input Sanitisation: URL decoding and canonicalisation to prevent malicious requests

File Serving

  • MIME Type Detection: Automatic content-type detection for common file formats including HTML, CSS, JavaScript, images, fonts, audio/video, and documents
  • Streaming Support: Large files are streamed in configurable chunks to manage memory usage
  • Index Files: Automatic serving of index files in priority order: index.html, index.htm, default.html, default.htm
  • Directory Listing: Optional automatic directory browsing with formatted HTML output

Error Handling

The server provides comprehensive error handling with styled HTML error pages for:

  • 400 Bad Request (malformed requests, invalid paths)
  • 403 Forbidden (blocked file types, disabled directory listing)
  • 404 Not Found (missing files or directories)
  • 405 Method Not Allowed (unsupported HTTP methods)
  • 429 Too Many Requests (rate limit exceeded)
  • 500 Internal Server Error (file read errors)

HTTP Compliance

  • HTTP/1.1 Support: Full HTTP/1.1 protocol implementation
  • Method Support: GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS methods supported via routing system
  • Header Processing: Complete request and response header handling
  • Connection Management: Proper connection lifecycle management
  • Status Codes: Appropriate HTTP status codes for all response conditions

Configuration Examples

Basic File Server

local server = httpServer.start({
   port = 8080,
   folder = 'parasol:docs/html/',
   verbose = true
})

Basic API Server with Routing

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 }
   }
})

Secure Localhost Server

local server = httpServer.start({
   port = 3000,
   folder = '/path/to/secure/files/',
   bind = '127.0.0.1',
   autoIndex = false,
   rateLimit = 50,
   timeout = 15
})

High-Performance Server

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
})

RESTful API Server

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)
         deleteUser(req.params.id)
         res.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 }
   }
})