010. HttpServer Production Readiness Fix
Overview
Users reported that the HTTP server crashed or became unresponsive after running for several days. After four rounds of iterative fixes, a total of 3 critical + 5 high + 7 medium = 15 BUGs were identified. 12 were fixed (BUG-1~10, BUG-15), 2 were mitigated (BUG-11, BUG-12), 1 was not fixed (BUG-13), and 1 was a feature gap (BUG-14).
The user's core concern was "HTTP connections cause memory to increase bit by bit, never decreasing, eventually crashing the program." The four rounds of fixes addressed this goal sequentially: fatal crashes → memory leaks → empty session accumulation → As New implicit references → SSE resource residuals.
Problem Overview
| ID | Level | Issue | Root Cause | Impact | Status |
|---|---|---|---|---|---|
| BUG-1 | Critical | Connection pool index drift | Collection.Remove causes subsequent indices to shift forward | Data sent to wrong client, GPF crash | Fixed |
| BUG-2 | Critical | Zombie connection infinite accumulation | No idle timeout + circular reference chain | Memory continuously grows → OOM | Fixed |
| BUG-3 | Critical | No request body size limit | No Content-Length cap | Malicious requests exhaust memory | Fixed |
| BUG-4 | High | recvBuffer never shrinks | Array doesn't release after large request | Long connection hidden memory leak | Fixed |
| BUG-5 | High | Circular reference chain | cHttpServer↔cClientCallback↔Response mutual references | COM objects never released | Fixed |
| BUG-6 | High | FindCRLFCRLF returns 0 ambiguity | Position 0 confused with "not found" | Specific requests can never be parsed | Fixed |
| BUG-7 | High | No maximum connection limit | No server-side protection | Resource exhaustion | Fixed |
| BUG-8 | High | SendData has no error handling | Sending after disconnect throws exception | Process crash | Fixed |
| BUG-9 | Medium | SSE.Entry failure doesn't clean connection pool | CloseSck triggered but m_cConnPool residue remains | Connection pool leak | Fixed |
| BUG-10 | Medium | SSE.Data shared mutable state | Data corruption on reentry | Data loss | Fixed |
| BUG-11 | Medium | Session cleanup probability 1% too low | Session accumulation in low traffic | Memory leak | Mitigated (cTimer) |
| BUG-12 | Medium | StopMe destroys shared objects | Router/SSE/Database set to Nothing | Configuration lost after restart | Mitigated |
| BUG-13 | Medium | No chunked transfer encoding support | Chunked transfer parsing not implemented | Specific clients hang | Not fixed |
| BUG-14 | Medium | Static file cache not updated | WebRoot only scanned at startup | Files added at runtime invisible | Not fixed |
| BUG-15 | Medium | SSE CloseClient doesn't close Socket | Only cleans dictionary but doesn't close Socket | Connection residue | Fixed |
Round 1: Critical/High BUG Fixes
BUG-1: Connection Pool Collection Index Drift
Symptom: After running for a while, data gets sent to the wrong client, or accessing released objects causes GPF crashes.
Root Cause: m_cConnPool uses Collection to store connections, with each cClientCallback.Index recording its position. When a connection disconnects and m_cConnPool.Remove Index is called, all subsequent items' indices shift forward, but oCallback.Index retains the old value.
Connection pool: [A(1), B(2), C(3), D(4)]
B disconnects → Remove 2
Connection pool: [A(1), C(2), D(3)] ← C and D indices changed
But C.Index is still 2, D.Index is still 3
If A disconnects Remove 1, C from 2→1, D from 3→2
But C.Index=2, D.Index=3 → m_cConnPool(3) out of bounds or wrong object!Fix: Collection → Scripting.Dictionary, using hSocket as Key.
' Before fix
Private m_cConnPool As New Collection ' Sequential index
oCallback.Index = m_cConnPool.Count
m_cConnPool.Add oCallback
m_cConnPool.Remove Index
' After fix
Private m_cConnPool As Scripting.Dictionary ' hSocket as Key
m_cConnPool.Add hSocket, oCallback
m_cConnPool.Remove hSocketBUG-2: Zombie Connection Infinite Accumulation
Symptom: HTTP server memory continuously grows after running for days, eventually OOM crash.
Root Cause: Client disconnects abnormally without sending TCP FIN, so Socket_CloseSck event never triggers. Circular reference chain prevents COM reference count from reaching zero:
cHttpServer ← m_cConnPool ← cClientCallback ← Parent ──→ cHttpServer
cClientCallback ← Context ← Response ← fClient ──→ cClientCallbackFix:
- Added
LastActivityfield, updated on each data arrival - Added
IdleTimeoutSecondsproperty (default 120 seconds) - Added
CleanupIdleConnections()method, iterates connection pool to release timed-out connections - Integrated
cTimerbuilt-in timer, auto-cleanup every 30 seconds by default
BUG-3: No Request Body Size Limit
Root Cause: Server accepts data unconditionally, AppendToBuffer continuously ReDim Preserve expanding the buffer.
Fix:
- Added
MaxRequestSizeproperty (default 10MB) - Check buffer + new data against limit in
OnDataArrival - Check Content-Length against limit in
ContainsCompleteRequest - Added
State413response
BUG-4: recvBuffer Never Shrinks
Root Cause: After processing a large request, recvBuffer() array retains its expanded size.
Fix: In RemoveFromBuffer, Erase recvBuffer when buffer is empty; when capacity > 64KB and utilization < 1/4, ReDim Preserve to shrink.
BUG-5: Circular Reference Chain
Fix: Release() thoroughly releases all references, Erase recvBuffer, resets hSocket = 0. Class_Terminate calls StopMe.
BUG-6: FindCRLFCRLF Returns 0 Ambiguity
Root Cause: CRLFCRLF at position 0 returns 0, not found also returns 0.
Fix: Return -1 when not found, synchronize ContainsCompleteRequest and ExtractOneRequest.
BUG-7: No Maximum Connection Limit
Fix: Added MaxConnections property (default 1000), Disconnect = True when exceeded.
BUG-8: SendData Has No Error Handling
Fix: SendBodyByte and SendHeader with On Error Resume Next, added m_HasSent to prevent duplicate sends.
Round 2: Memory Leak Explicit Cleanup
Symptoms
After Round 1 fixes, stress testing showed memory still growing at ~10MB/s: 10 connections, 900 QPS, memory reached 1.8GB in 339 seconds.
Root Cause
ProcessHttpRequest creates ~18+ COM objects per request. After the request completes, only COM reference counting waits for reclamation, which can't keep up at high concurrency:
- Response.fClient → oCallback circular reference:
oCallback → Context → Response → fClient → oCallback - Request internal large objects not promptly cleaned: 5 Dictionaries + PathInfoList + byte arrays
- VB6 reference count reclamation lags at high frequency: 900 QPS × 18 objects = 16,200 COM objects/second
Fix
| File | Change |
|---|---|
cHttpServer.cls | Call Context.ReleaseRequest + Request.ClearRequest before all exit points in ProcessHttpRequest (State413, Options, normal exit, EH) |
cHttpServerContext.cls | Added ReleaseRequest(): breaks Response.fClient circular reference, cleans Request/Response/Cookies/Session, preserves SSE/UserData/TimeUse |
cHttpServerRequest.cls | Added ClearRequest(): RemoveAll all Dictionaries, Erase byte arrays; Class_Terminate for insurance cleanup |
cHttpServerResponse.cls | Added Class_Terminate: Set Client = Nothing, Header.RemoveAll, release JsonInst |
Cleanup Chain
ProcessHttpRequest request processing complete
│
├─ Context.ReleaseRequest()
│ ├─ Response.fClient = Nothing ← Break circular reference
│ ├─ Request.ClearRequest() ← Clear large objects
│ ├─ Set Request = Nothing
│ ├─ Set Response = Nothing
│ ├─ Cookies.Clear + Set Cookies = Nothing
│ ├─ Set Session = Nothing
│ └─ UserData.RemoveAll
│
└─ Set response = Nothing: Set Request = NothingRound 3: Session Accumulation + Class-Level As New Full Fix
Symptoms
After Round 2 fixes, memory leak was approximately 5-10MB/s.
Root Cause Analysis
Root Cause 1: Session auto-generates sessionID causing empty session accumulation
cHttpServerSession.sessionID Property Get auto-generates a GUID when pvSessionID is empty. ProcessHttpRequest uses If Session.sessionID <> "" Then to determine whether to store — every request without cookies triggers generation, the condition is always True, empty sessions are permanently stored in m_Sessions.
900 QPS × 60 seconds × 20 minutes = 1,080,000 empty sessions, approximately 1-2GB.
Root Cause 2: Class-level As New implicit creation prevents explicit release
VB6's Dim X As New ClassName: Set X = Nothing auto-recreates on next access, objects can never be truly destroyed. Impact scope:
| File | As New Field Count |
|---|---|
cHttpServer.cls | 9 |
cSSE.cls | 7 |
cHttpServerRouter.cls | 8 |
cHttpServerSvr.cls | 4 |
cHttpServerSession.cls | 1 |
cHttpServerRouteBefore.cls | 1 |
cHttpServerRouterAfter.cls | 1 |
Fix
Core Fix: Session Accumulation
| File | Change |
|---|---|
cHttpServerSession.cls | sessionID Property Get no longer auto-generates GUID, only returns pvSessionID; added HasID property; Data changed to lazy creation (EnsureData); added Release(); added Class_Terminate |
cHttpServer.cls | If Session.HasID Then replaces If Session.sessionID <> "" Then; explicit cleanup of local variables |
Class-Level As New Full Conversion
All 7 files changed to As + Class_Initialize explicit Set = New + Class_Terminate explicit Set = Nothing.
Also fixed BUG-9 in cSSE.cls (cleaning up SSE references set when Entry fails).
Cleanup Chain (Updated)
ProcessHttpRequest request processing complete
│
├─ Context.ReleaseRequest()
│ ├─ Response.fClient = Nothing
│ ├─ Request.ClearRequest()
│ ├─ Set Request = Nothing
│ ├─ Set Response = Nothing
│ ├─ Cookies.Clear + Set Cookies = Nothing
│ ├─ Set Session = Nothing
│ └─ UserData.RemoveAll
│
├─ Set Session = Nothing ← No longer waiting for Sub exit
├─ Set Cookies = Nothing
└─ Set response = Nothing: Set Request = NothingSession New Logic: Only when Session.HasID = True will Session be stored in m_Sessions, have Cookie written, and be persisted.
Round 4: Method-Level As New + SSE BUG-15/BUG-10
Method-Level As New Local Variable Full Conversion
The previous round fixed class-level fields, but method bodies still had 11 Dim X As New instances. The As New auto-recreation mechanism can prevent reclamation in exception paths.
| File | Count | Specific Changes |
|---|---|---|
cHttpServer.cls | 4 | CI, keysToClose, keysToRemove, sess → all explicit |
cHttpServerSession.cls | 4 | Json, SessionData, DataCopy in Serialize/Deserialize → explicit |
cHttpServerResponse.cls | 1 | Dic in Json method → explicit |
cHttpServerCookies.cls | 1 | CK in Cookie Property Get → explicit |
cHttpServerRouter.cls | 1 | item in Add method → explicit |
BUG-15: SSE CloseClient Doesn't Close Socket
Original Implementation Issues:
- When called via
hSocketparameter (most common), only checks existence then GoTo exit — no cleanup at all - When called via
ToUser().CloseClient(GoSub Start path), cleans dictionary but doesn't close Socket
Consequence: All actively closed SSE clients have Sockets that never close, cClientCallback never releases.
Fix: Refactored to unified cleanup path:
- Trigger
OnCloseuser event - Clear SSE context references
- Remove from Clients dictionary (prevent reentry)
- Clean up user/group mappings
Client.Socket.CloseSckcloses Socket, triggering cClientCallback complete cleanup flow
BUG-10: SSE.Data Shared Mutable State Reentry
Original Issue: Send method uses class-level shared Data dictionary. SendData may trigger Winsock message pump causing reentry.
Fix: Send snapshots Data to local SendData Dictionary before encoding, then immediately Data.RemoveAll. Encoding and sending use the safe copy.
Modified Files Summary (All Four Rounds)
| File | R1 | R2 | R3 | R4 | Change Summary |
|---|---|---|---|---|---|
src\Socket\cClientCallback.cls | ● | Index→hSocket; LastActivity; Release thorough cleanup; recvBuffer shrink | |||
src\HttpServer\cHttpServer.cls | ● | ● | ● | ● | Collection→Dictionary; 6 safety properties; FindCRLFCRLF -1; cTimer integration; ReleaseRequest/ClearRequest; Session.HasID; 13 As New→explicit |
src\HttpServer\cHttpServerResponse.cls | ● | ● | ● | SendData error protection; m_HasSent; State413; Class_Terminate; 1 As New→explicit | |
src\HttpServer\cHttpServerContext.cls | ● | ● | ReleaseRequest(); break circular references | ||
src\HttpServer\cHttpServerRequest.cls | ● | ● | ClearRequest(); Class_Terminate | ||
src\HttpServer\cHttpServerCookies.cls | ● | ● | 1 As New→explicit | ||
src\HttpServer\cHttpServerSession.cls | ● | ● | sessionID no auto-generate; HasID; EnsureData lazy creation; Release(); Class_Terminate; 5 As New→explicit | ||
src\HttpServer\cHttpServerSvr.cls | ● | 4 As New→explicit; Class_Initialize/Terminate | |||
src\HttpServer\cHttpServerRouter.cls | ● | ● | 8 class-level + 1 method-level As New→explicit; Class_Initialize/Terminate | ||
src\HttpServer\cHttpServerRouteBefore.cls | ● | 1 As New→explicit; Class_Initialize/Terminate | |||
src\HttpServer\cHttpServerRouterAfter.cls | ● | 1 As New→explicit; Class_Initialize/Terminate | |||
src\SSE\cSSE.cls | ● | ● | 7 class-level As New→explicit; BUG-9 Entry failure cleanup; BUG-15 CloseClient refactor+close Socket; BUG-10 Send snapshot for reentry; Class_Initialize/Terminate |
Total modified: 12 .cls files
New Public API Summary (All Four Rounds)
cHttpServer Properties/Methods
| Property/Method | Type | Default | Round | Description |
|---|---|---|---|---|
MaxConnections | Long | 1000 | R1 | Max concurrent connections |
MaxRequestSize | Long | 10485760 (10MB) | R1 | Max request body bytes, disconnect and return 413 when exceeded |
IdleTimeoutSeconds | Long | 120 | R1 | Idle connection timeout seconds, auto-close when exceeded |
ConnectionCount | Property Get | - | R1 | Current active connection count (read-only) |
CleanupIdleConnections() | Sub | - | R1 | Manually clean up idle connections |
CleanupExpiredSessions() | Sub | - | R1 | Manually clean up expired sessions |
CleanupTimerInterval | Property Get/Let | 30000 (30s) | R1 | Built-in cleanup timer interval (ms) |
State413() | Sub | - | R1 | 413 Payload Too Large response |
cHttpServerSession Properties/Methods
| Property/Method | Type | Round | Description |
|---|---|---|---|
HasID | Property Get (Boolean) | R3 | Check if sessionID exists (i.e., data has been written) |
Release() | Friend Sub | R3 | Explicitly release Data Dictionary, clear sessionID |
cHttpServerContext Methods
| Method | Round | Description |
|---|---|---|
ReleaseRequest() | R2 | Break Response.fClient circular reference, clean Request/Response/Cookies/Session |
cHttpServerRequest Methods
| Method | Round | Description |
|---|---|---|
ClearRequest() | R2 | RemoveAll all Dictionaries, Erase byte arrays |
Built-in Timer Cleanup Mechanism
After the server starts, a cTimer timer is automatically created (depends on ToolsTimer.bas module), triggering every 30 seconds by default:
CleanupIdleConnections— Cleans zombie connections exceedingIdleTimeoutSecondsCleanupExpiredSessions— Cleans expired sessions
CleanupTimerInterval can adjust the interval, and changes while running automatically restart the timer. The timer is automatically destroyed on stop.
As New Conversion Complete List
This was the largest systematic change across the four rounds of fixes, eliminating the VB6 As New implicit auto-recreation that prevents objects from ever being truly released.
Class-Level Fields (Round 3)
| File | Count |
|---|---|
cHttpServer.cls | 9 |
cSSE.cls | 7 |
cHttpServerRouter.cls | 8 |
cHttpServerSvr.cls | 4 |
cHttpServerSession.cls | 1 |
cHttpServerRouteBefore.cls | 1 |
cHttpServerRouterAfter.cls | 1 |
| Total | 31 |
Method-Level Local Variables (Round 4)
| File | Count |
|---|---|
cHttpServer.cls | 4 |
cHttpServerSession.cls | 4 |
cHttpServerResponse.cls | 1 |
cHttpServerCookies.cls | 1 |
cHttpServerRouter.cls | 1 |
| Total | 11 |
Conversion Pattern
' Before fix
Private m_Sessions As New Scripting.Dictionary
Dim Dic As New Scripting.Dictionary
' After fix (class-level)
Private m_Sessions As Scripting.Dictionary ' Set = New in Class_Initialize
' Set = Nothing in Class_Terminate
' After fix (method-level)
Dim Dic As Scripting.Dictionary
Set Dic = New Scripting.DictionaryVerification Results
After four rounds of fixes, all 15 HttpServer + SSE .cls files have no code-level As New remaining (only preserved in comments for explanation).
Performance Impact Assessment
| Fix Item | Performance Impact | Round |
|---|---|---|
| Collection → Dictionary | Lookup O(1), faster than Collection.Item(index) | R1 |
| CleanupIdleConnections | Built-in timer every 30s + manual, iteration O(N) | R1 |
| Buffer shrinking | Only triggers when capacity > 64KB and utilization < 25% | R1 |
| MaxRequestSize check | One Long comparison per data arrival O(1) | R1 |
| MaxConnections check | Dictionary.Count read O(1) | R1 |
| SendData error protection | On Error Resume Next only has overhead on exceptions | R1 |
| ReleaseRequest/ClearRequest | Explicit release at request end, reduces COM reclamation pressure | R2 |
| Session.HasID check | Boolean property read, O(1) | R3 |
| SSE Send snapshot | Copy Data to local Dict, O(N) (N = Data entries, typically < 10) | R4 |
Overall performance impact is negligible.
Remaining Issues
| ID | Issue | Status | Description |
|---|---|---|---|
| BUG-11 | Session cleanup probability 1% | Mitigated | cTimer calls CleanupExpiredSessions every 30s, no longer relies on random probability |
| BUG-12 | StopMe destroys shared objects | Mitigated | Round 1 fix: StopMe no longer destroys shared objects, but state restoration after restart still needs attention |
| BUG-13 | No chunked transfer encoding support | Not fixed | Need to implement chunked transfer parser, impact is small |
| BUG-14 | Static file cache not updated | Not fixed | WebRoot dictionary scanned once at startup, files added at runtime require restart |
| - | cWinSock.cls Collection.Exists | Not fixed | m_cClients declared as Collection but calls .Exists(), will error at runtime |
Upgrade Guide
Minimum upgrade requirement: Replace the following 12 .cls files
src\Socket\cClientCallback.cls
src\HttpServer\cHttpServer.cls
src\HttpServer\cHttpServerResponse.cls
src\HttpServer\cHttpServerContext.cls
src\HttpServer\cHttpServerRequest.cls
src\HttpServer\cHttpServerCookies.cls
src\HttpServer\cHttpServerSession.cls
src\HttpServer\cHttpServerSvr.cls
src\HttpServer\cHttpServerRouter.cls
src\HttpServer\cHttpServerRouteBefore.cls
src\HttpServer\cHttpServerRouterAfter.cls
src\SSE\cSSE.clsProject dependency: cTimer integration requires ToolsTimer.bas module to be included in the project
No modification needed: The following files were audited and require no changes
cHttpServerApp.cls— Empty classcHttpServerRouteItem.cls— Pure simple typescHttpServerCookieAttr.cls— Pure simple typescHttpServerClientInfo.cls— Pure simple typescSSEContext.cls— Already has Class_Terminate, no As New
Configuration recommendations:
' Pre-startup configuration (recommended production values)
Server.MaxConnections = 500 ' Adjust based on memory
Server.MaxRequestSize = 5242880 ' 5MB
Server.IdleTimeoutSeconds = 60 ' 1 minute
Server.CleanupTimerInterval = 30000 ' 30 seconds (default, can leave unchanged)Fix Date: 2026-06-13 Fix Version: v1.0.0.423 Affected Versions: All historical versions using Collection connection pool
Verification Results
2026-06-13 User stress test verification passed: After four rounds of fixes, all memory leak issues have been eliminated. HttpServer is now ready for production use.

The timer recycler is healthy now. When there are no requests, memory is reclaimed to 3.5MB, nearly half.

Four Rounds Fix Overview
| Round | Focus | Files Modified | Bugs Fixed |
|---|---|---|---|
| Round 1 | Critical/High BUG fixes | 6 | BUG-1~8 |
| Round 2 | Memory leak explicit cleanup | 4 | ReleaseRequest/ClearRequest |
| Round 3 | Session accumulation + class-level As New | 7 | Session HasID + 31 As New + BUG-9 |
| Round 4 | Method-level As New + SSE BUG-15/10 | 6 | 11 As New + BUG-15 + BUG-10 |
| Total | 12 files | BUG-1~10, 15 (12 BUGs) + 42 As New |