Skip to content

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

IDLevelIssueRoot CauseImpactStatus
BUG-1CriticalConnection pool index driftCollection.Remove causes subsequent indices to shift forwardData sent to wrong client, GPF crashFixed
BUG-2CriticalZombie connection infinite accumulationNo idle timeout + circular reference chainMemory continuously grows → OOMFixed
BUG-3CriticalNo request body size limitNo Content-Length capMalicious requests exhaust memoryFixed
BUG-4HighrecvBuffer never shrinksArray doesn't release after large requestLong connection hidden memory leakFixed
BUG-5HighCircular reference chaincHttpServer↔cClientCallback↔Response mutual referencesCOM objects never releasedFixed
BUG-6HighFindCRLFCRLF returns 0 ambiguityPosition 0 confused with "not found"Specific requests can never be parsedFixed
BUG-7HighNo maximum connection limitNo server-side protectionResource exhaustionFixed
BUG-8HighSendData has no error handlingSending after disconnect throws exceptionProcess crashFixed
BUG-9MediumSSE.Entry failure doesn't clean connection poolCloseSck triggered but m_cConnPool residue remainsConnection pool leakFixed
BUG-10MediumSSE.Data shared mutable stateData corruption on reentryData lossFixed
BUG-11MediumSession cleanup probability 1% too lowSession accumulation in low trafficMemory leakMitigated (cTimer)
BUG-12MediumStopMe destroys shared objectsRouter/SSE/Database set to NothingConfiguration lost after restartMitigated
BUG-13MediumNo chunked transfer encoding supportChunked transfer parsing not implementedSpecific clients hangNot fixed
BUG-14MediumStatic file cache not updatedWebRoot only scanned at startupFiles added at runtime invisibleNot fixed
BUG-15MediumSSE CloseClient doesn't close SocketOnly cleans dictionary but doesn't close SocketConnection residueFixed

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: CollectionScripting.Dictionary, using hSocket as Key.

vb
' 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 hSocket

BUG-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 ──→ cClientCallback

Fix:

  1. Added LastActivity field, updated on each data arrival
  2. Added IdleTimeoutSeconds property (default 120 seconds)
  3. Added CleanupIdleConnections() method, iterates connection pool to release timed-out connections
  4. Integrated cTimer built-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:

  1. Added MaxRequestSize property (default 10MB)
  2. Check buffer + new data against limit in OnDataArrival
  3. Check Content-Length against limit in ContainsCompleteRequest
  4. Added State413 response

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:

  1. Response.fClient → oCallback circular reference: oCallback → Context → Response → fClient → oCallback
  2. Request internal large objects not promptly cleaned: 5 Dictionaries + PathInfoList + byte arrays
  3. VB6 reference count reclamation lags at high frequency: 900 QPS × 18 objects = 16,200 COM objects/second

Fix

FileChange
cHttpServer.clsCall Context.ReleaseRequest + Request.ClearRequest before all exit points in ProcessHttpRequest (State413, Options, normal exit, EH)
cHttpServerContext.clsAdded ReleaseRequest(): breaks Response.fClient circular reference, cleans Request/Response/Cookies/Session, preserves SSE/UserData/TimeUse
cHttpServerRequest.clsAdded ClearRequest(): RemoveAll all Dictionaries, Erase byte arrays; Class_Terminate for insurance cleanup
cHttpServerResponse.clsAdded 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 = Nothing

Round 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:

FileAs New Field Count
cHttpServer.cls9
cSSE.cls7
cHttpServerRouter.cls8
cHttpServerSvr.cls4
cHttpServerSession.cls1
cHttpServerRouteBefore.cls1
cHttpServerRouterAfter.cls1

Fix

Core Fix: Session Accumulation

FileChange
cHttpServerSession.clssessionID 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.clsIf 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 = Nothing

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

FileCountSpecific Changes
cHttpServer.cls4CI, keysToClose, keysToRemove, sess → all explicit
cHttpServerSession.cls4Json, SessionData, DataCopy in Serialize/Deserialize → explicit
cHttpServerResponse.cls1Dic in Json method → explicit
cHttpServerCookies.cls1CK in Cookie Property Get → explicit
cHttpServerRouter.cls1item in Add method → explicit

BUG-15: SSE CloseClient Doesn't Close Socket

Original Implementation Issues:

  • When called via hSocket parameter (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:

  1. Trigger OnClose user event
  2. Clear SSE context references
  3. Remove from Clients dictionary (prevent reentry)
  4. Clean up user/group mappings
  5. Client.Socket.CloseSck closes 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)

FileR1R2R3R4Change Summary
src\Socket\cClientCallback.clsIndex→hSocket; LastActivity; Release thorough cleanup; recvBuffer shrink
src\HttpServer\cHttpServer.clsCollection→Dictionary; 6 safety properties; FindCRLFCRLF -1; cTimer integration; ReleaseRequest/ClearRequest; Session.HasID; 13 As New→explicit
src\HttpServer\cHttpServerResponse.clsSendData error protection; m_HasSent; State413; Class_Terminate; 1 As New→explicit
src\HttpServer\cHttpServerContext.clsReleaseRequest(); break circular references
src\HttpServer\cHttpServerRequest.clsClearRequest(); Class_Terminate
src\HttpServer\cHttpServerCookies.cls1 As New→explicit
src\HttpServer\cHttpServerSession.clssessionID no auto-generate; HasID; EnsureData lazy creation; Release(); Class_Terminate; 5 As New→explicit
src\HttpServer\cHttpServerSvr.cls4 As New→explicit; Class_Initialize/Terminate
src\HttpServer\cHttpServerRouter.cls8 class-level + 1 method-level As New→explicit; Class_Initialize/Terminate
src\HttpServer\cHttpServerRouteBefore.cls1 As New→explicit; Class_Initialize/Terminate
src\HttpServer\cHttpServerRouterAfter.cls1 As New→explicit; Class_Initialize/Terminate
src\SSE\cSSE.cls7 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/MethodTypeDefaultRoundDescription
MaxConnectionsLong1000R1Max concurrent connections
MaxRequestSizeLong10485760 (10MB)R1Max request body bytes, disconnect and return 413 when exceeded
IdleTimeoutSecondsLong120R1Idle connection timeout seconds, auto-close when exceeded
ConnectionCountProperty Get-R1Current active connection count (read-only)
CleanupIdleConnections()Sub-R1Manually clean up idle connections
CleanupExpiredSessions()Sub-R1Manually clean up expired sessions
CleanupTimerIntervalProperty Get/Let30000 (30s)R1Built-in cleanup timer interval (ms)
State413()Sub-R1413 Payload Too Large response

cHttpServerSession Properties/Methods

Property/MethodTypeRoundDescription
HasIDProperty Get (Boolean)R3Check if sessionID exists (i.e., data has been written)
Release()Friend SubR3Explicitly release Data Dictionary, clear sessionID

cHttpServerContext Methods

MethodRoundDescription
ReleaseRequest()R2Break Response.fClient circular reference, clean Request/Response/Cookies/Session

cHttpServerRequest Methods

MethodRoundDescription
ClearRequest()R2RemoveAll 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:

  1. CleanupIdleConnections — Cleans zombie connections exceeding IdleTimeoutSeconds
  2. CleanupExpiredSessions — 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)

FileCount
cHttpServer.cls9
cSSE.cls7
cHttpServerRouter.cls8
cHttpServerSvr.cls4
cHttpServerSession.cls1
cHttpServerRouteBefore.cls1
cHttpServerRouterAfter.cls1
Total31

Method-Level Local Variables (Round 4)

FileCount
cHttpServer.cls4
cHttpServerSession.cls4
cHttpServerResponse.cls1
cHttpServerCookies.cls1
cHttpServerRouter.cls1
Total11

Conversion Pattern

vb
' 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.Dictionary

Verification 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 ItemPerformance ImpactRound
Collection → DictionaryLookup O(1), faster than Collection.Item(index)R1
CleanupIdleConnectionsBuilt-in timer every 30s + manual, iteration O(N)R1
Buffer shrinkingOnly triggers when capacity > 64KB and utilization < 25%R1
MaxRequestSize checkOne Long comparison per data arrival O(1)R1
MaxConnections checkDictionary.Count read O(1)R1
SendData error protectionOn Error Resume Next only has overhead on exceptionsR1
ReleaseRequest/ClearRequestExplicit release at request end, reduces COM reclamation pressureR2
Session.HasID checkBoolean property read, O(1)R3
SSE Send snapshotCopy Data to local Dict, O(N) (N = Data entries, typically < 10)R4

Overall performance impact is negligible.


Remaining Issues

IDIssueStatusDescription
BUG-11Session cleanup probability 1%MitigatedcTimer calls CleanupExpiredSessions every 30s, no longer relies on random probability
BUG-12StopMe destroys shared objectsMitigatedRound 1 fix: StopMe no longer destroys shared objects, but state restoration after restart still needs attention
BUG-13No chunked transfer encoding supportNot fixedNeed to implement chunked transfer parser, impact is small
BUG-14Static file cache not updatedNot fixedWebRoot dictionary scanned once at startup, files added at runtime require restart
-cWinSock.cls Collection.ExistsNot fixedm_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.cls

Project 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 class
  • cHttpServerRouteItem.cls — Pure simple types
  • cHttpServerCookieAttr.cls — Pure simple types
  • cHttpServerClientInfo.cls — Pure simple types
  • cSSEContext.cls — Already has Class_Terminate, no As New

Configuration recommendations:

vb
' 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.

All leak tests fixed

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

Timer recycler healthy


Four Rounds Fix Overview

RoundFocusFiles ModifiedBugs Fixed
Round 1Critical/High BUG fixes6BUG-1~8
Round 2Memory leak explicit cleanup4ReleaseRequest/ClearRequest
Round 3Session accumulation + class-level As New7Session HasID + 31 As New + BUG-9
Round 4Method-level As New + SSE BUG-15/10611 As New + BUG-15 + BUG-10
Total12 filesBUG-1~10, 15 (12 BUGs) + 42 As New

VB6 and LOGO copyright of Microsoft Corporation