This document is a developer reference for extending PureSimpleHTTPServer. The target audience is PureBasic developers who have the source and want to add new features — middleware, CLI flags, MIME types, or embedded asset workflows.
Prerequisites: Read the Developer Guide for the middleware architecture and BUILD_OUR_HTTP_SERVER.md for a walkthrough of
main.pb.
CLI flags are parsed in Config.pbi by ParseCLI(*cfg.ServerConfig) and stored
in the ServerConfig structure defined in Types.pbi.
Structure ServerConfig
; ... existing fields ...
RateLimit.i ; #True to enable rate limiting headers
EndStructure
Procedure LoadDefaults(*cfg.ServerConfig)
; ... existing defaults ...
*cfg\RateLimit = #False
EndProcedure
; Boolean flag — no argument
ElseIf param = "--rate-limit"
*cfg\RateLimit = #True
If *cfg\RateLimit
; apply rate limit headers
EndIf
The middleware chain is the primary extension point in v2.x. All request handling
flows through BuildChain() in Middleware.pbi.
All middleware share the same signature:
Procedure.i Middleware_YourFeature(*req.HttpRequest, *resp.ResponseBuffer, *mCtx.MiddlewareContext)
Protected *cfg.ServerConfig = *mCtx\Config
; Your logic here. Use *req for the request, *cfg for configuration.
; Option A: short-circuit (handled)
; Fill *resp, return #True.
;
; Option B: pass through
; ProcedureReturn CallNext(*req, *resp, *mCtx)
ProcedureReturn CallNext(*req, *resp, *mCtx)
EndProcedure
Example — RateLimit headers (post-processing):
Procedure.i Middleware_RateLimit(*req.HttpRequest, *resp.ResponseBuffer, *mCtx.MiddlewareContext)
; Let downstream produce the response first
Protected result.i = CallNext(*req, *resp, *mCtx)
; Then append rate limit headers to whatever was produced
If *resp\Handled
*resp\Headers + "X-RateLimit-Remaining: 99" + #CRLF$
EndIf
ProcedureReturn result
EndProcedure
Add one line at the correct position in BuildChain() (in Middleware.pbi):
Procedure BuildChain()
g_ChainCount = 0
RegisterMiddleware(@Middleware_Rewrite())
RegisterMiddleware(@Middleware_HealthCheck())
RegisterMiddleware(@Middleware_IndexFile())
RegisterMiddleware(@Middleware_CleanUrls())
RegisterMiddleware(@Middleware_SpaFallback())
RegisterMiddleware(@Middleware_HiddenPath())
RegisterMiddleware(@Middleware_Cors())
RegisterMiddleware(@Middleware_BasicAuth())
RegisterMiddleware(@Middleware_SecurityHeaders())
RegisterMiddleware(@Middleware_RateLimit()) ; ← new
RegisterMiddleware(@Middleware_ETag304())
RegisterMiddleware(@Middleware_GzipSidecar())
RegisterMiddleware(@Middleware_GzipCompress())
RegisterMiddleware(@Middleware_EmbeddedAssets())
RegisterMiddleware(@Middleware_FileServer())
RegisterMiddleware(@Middleware_DirectoryListing())
EndProcedure
Placement rules:
- Request modifiers go first (positions 1-4).
- Access control goes after modifiers (check the final path).
- Conditional responses go before terminal handlers (avoid I/O when possible).
- Post-processing middleware call
CallNext()first, then modify*resp. - Terminal handlers go last and do NOT call
CallNext()on success.
Create a ProcedureUnit in tests/test_middleware.pb:
ProcedureUnit RateLimit_AddsHeader()
Protected cfg.ServerConfig
InitTestCfg(@cfg)
Protected req.HttpRequest
req\Method = "GET" : req\Path = "/test"
Protected resp.ResponseBuffer
InitResp(@resp)
Protected mCtx.MiddlewareContext
InitMCtx(@mCtx, @cfg)
; Register a dummy terminal handler
g_ChainCount = 0
Protected result.i = Middleware_RateLimit(@req, @resp, @mCtx)
Assert(FindString(resp\Headers, "X-RateLimit-Remaining") > 0, "Rate limit header present")
FreeResp(@resp)
EndProcedureUnit
MimeTypes.pbi uses a single Select/Case block. Add a Case line:
Case "glb" : ProcedureReturn "model/gltf-binary"
Case "gltf" : ProcedureReturn "model/gltf+json"
The extension parameter is always lowercase without a leading dot. No other
changes needed — GetMimeType is called from both middleware and embedded assets.
For responses not covered by BuildResponseHeaders + body output:
Procedure SendBinaryResponse(connection.i, statusCode.i, contentType.s,
*data, dataLen.i)
Protected extra.s = "Content-Type: " + contentType + #CRLF$
SendNetworkString(connection, BuildResponseHeaders(statusCode, extra, dataLen),
#PB_Ascii)
If dataLen > 0
SendNetworkData(connection, *data, dataLen)
EndIf
EndProcedure
- File name: camelcase
.pbi(e.g.,AuthMiddleware.pbi). - Begin with
EnableExplicitand a comment header. - Private procedures use trailing underscore (e.g.,
HashPassword_()).
Insert after all dependencies are included. Also add to tests/TestCommon.pbi.
Only scalar Global variables are safe in PureUnit-tested modules. See
TESTING.md for why Global Dim and Global NewMap crash under PureUnit.
| Resource | Mutex | Notes |
|---|---|---|
| Log files, rotation state | g_LogMutex |
Both log files share one mutex |
| Rewrite rule arrays, cache | g_RewriteMutex |
Acquired by ApplyRewrites, LoadGlobalRules |
g_CloseList |
g_CloseMutex |
Worker threads push; main thread pops |
g_Config— written once beforeStartServer; read-only at runtime.g_Handler— set once beforeStartServer.g_Chain/g_ChainCount— set once byBuildChain()beforeStartServer.g_EmbeddedPack— set once byOpenEmbeddedPack.
- Declare a mutex as
Global .iin your module. - Create it in your
Init*()procedure. - Wrap every read and write with
LockMutex/UnlockMutex. - Document in
ARCHITECTURE.md's mutex inventory.
See BUILDING.md section 5 for the full embedded assets workflow:
UseZipPacker() + DataSection + IncludeBinary + OpenEmbeddedPack.