optimized dynamic body size impl, decluttering javadocs/comments

This commit is contained in:
Relism
2026-03-15 17:14:17 +01:00
parent b0d606cb5f
commit 94d029a631
17 changed files with 196 additions and 409 deletions
+73 -106
View File
@@ -4,70 +4,24 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="pre-module refactor"> <list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="refactored, pre-buffer reuse">
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash-bench/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-bench/pom.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash-bench/pom.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/Main.java" beforeDir="false" />
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/Main.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/resources/simplelogger.properties" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/flash/pom.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/LazyBody.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/LazyBody.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/encodings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/encodings.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/PathParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/PathParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CLAUDE.md" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/Flash.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Flash.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/HttpServerConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/ContentType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/HttpMethod.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpMethod.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/HttpStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/HeaderMap.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/LazyBody.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/LazyBody.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/PathParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/PathParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/Request.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/RequestHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/RequestLine.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestLine.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/Response.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/SimpleHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/SimpleHandler.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/GlobalRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/PathUtils.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PathUtils.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/Route.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Route.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/template/ByteTemplate.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ByteTemplate.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/template/ErrorPages.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ErrorPages.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/html/default_404.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_404.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/html/default_exception.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_exception.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/logo.png" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/logo.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/HttpServerConcurrencyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/HttpServerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/RequestParserTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/ContentTypeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/ContentTypeTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/HttpMethodTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpMethodTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/HttpStatusTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpStatusTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/HeaderMapTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/HeaderMapTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/PathParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/PathParamsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/QueryParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/QueryParamsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/RequestLineTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestLineTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/RequestTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/ResponseTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/ResponseTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/SimpleHandlerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/SimpleHandlerTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/AbstractRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/AbstractRouterTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/GlobalRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/GlobalRouterTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/PathUtilsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/PathUtilsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/template/ByteTemplateTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ByteTemplateTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/template/ErrorPagesTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ErrorPagesTest.java" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -119,55 +73,59 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;Application.ExternalBenchmark.executor&quot;: &quot;Run&quot;, "Application.ExternalBenchmark.executor": "Run",
&quot;Application.Main.executor&quot;: &quot;Run&quot;, "Application.Main.executor": "Run",
&quot;Application.dev.relism.bench.Main.executor&quot;: &quot;Run&quot;, "Application.dev.relism.bench.Main.executor": "Run",
&quot;JUnit.RequestParserTest.executor&quot;: &quot;Run&quot;, "JUnit.RequestParserTest.executor": "Run",
&quot;JUnit.RequestParserTest.headers_caseInsensitive.executor&quot;: &quot;Debug&quot;, "JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
&quot;Maven.FlashPractice [test].executor&quot;: &quot;Run&quot;, "Maven.FlashPractice [test].executor": "Run",
&quot;Maven.flash [compile].executor&quot;: &quot;Run&quot;, "Maven.flash [compile].executor": "Run",
&quot;Maven.flash [install].executor&quot;: &quot;Run&quot;, "Maven.flash [install].executor": "Run",
&quot;Maven.flash-bench [clean].executor&quot;: &quot;Run&quot;, "Maven.flash [test].executor": "Run",
&quot;Maven.flash-bench [install].executor&quot;: &quot;Run&quot;, "Maven.flash-bench [clean].executor": "Run",
&quot;Maven.flash-bench [validate].executor&quot;: &quot;Run&quot;, "Maven.flash-bench [install].executor": "Run",
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;, "Maven.flash-bench [validate].executor": "Run",
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;, "ModuleVcsDetector.initialDetectionPerformed": "true",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.MCP Project settings loaded": "true",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "RunOnceActivity.typescript.service.memoryLimit.init": "true",
&quot;git-widget-placeholder&quot;: &quot;master&quot;, "SHARE_PROJECT_CONFIGURATION_FILES": "true",
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;, "git-widget-placeholder": "master",
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;, "ignore.virus.scanning.warn.message": "true",
&quot;last_opened_file_path&quot;: &quot;C:/Users/elorc/Documents/Coding/Java/practice/Flash&quot;, "kotlin-language-version-configured": "true",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;onboarding.tips.debug.path&quot;: &quot;C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java&quot;, "nodejs_package_manager_path": "npm",
&quot;project.structure.last.edited&quot;: &quot;Modules&quot;, "onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java",
&quot;project.structure.proportion&quot;: &quot;0.15&quot;, "project.structure.last.edited": "Modules",
&quot;project.structure.side.proportion&quot;: &quot;0.1150748&quot;, "project.structure.proportion": "0.15",
&quot;settings.editor.selected.configurable&quot;: &quot;ml.llm.LLMThirdPartyAiProvidersConfigurable&quot;, "project.structure.side.proportion": "0.1150748",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "settings.editor.selected.configurable": "ml.llm.LLMThirdPartyAiProvidersConfigurable",
"vue.rearranger.settings.migration": "true"
} }
}</component> }]]></component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" /> <recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" />
</key> </key>
<key name="MoveClassesOrPackagesDialog.RECENTS_KEY">
<recent name="dev.relism" />
</key>
</component> </component>
<component name="RunManager" selected="Application.ExternalBenchmark"> <component name="RunManager" selected="Application.ExternalBenchmark">
<configuration name="ExternalBenchmark" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true"> <configuration name="ExternalBenchmark" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="dev.relism.bench.ExternalBenchmark" /> <option name="MAIN_CLASS_NAME" value="dev.relism.ExternalBenchmark" />
<module name="flash-bench" /> <module name="flash-bench" />
<extension name="coverage"> <extension name="coverage">
<pattern> <pattern>
<option name="PATTERN" value="dev.relism.bench.*" /> <option name="PATTERN" value="dev.relism.*" />
<option name="ENABLED" value="true" /> <option name="ENABLED" value="true" />
</pattern> </pattern>
</extension> </extension>
@@ -193,7 +151,7 @@
<module name="flash-bench" /> <module name="flash-bench" />
<extension name="coverage"> <extension name="coverage">
<pattern> <pattern>
<option name="PATTERN" value="dev.relism.bench.*" /> <option name="PATTERN" value="dev.relism.*" />
<option name="ENABLED" value="true" /> <option name="ENABLED" value="true" />
</pattern> </pattern>
</extension> </extension>
@@ -235,7 +193,7 @@
<workItem from="1773536904928" duration="395000" /> <workItem from="1773536904928" duration="395000" />
<workItem from="1773537302622" duration="150000" /> <workItem from="1773537302622" duration="150000" />
<workItem from="1773537489622" duration="5048000" /> <workItem from="1773537489622" duration="5048000" />
<workItem from="1773576241900" duration="5173000" /> <workItem from="1773576241900" duration="14674000" />
</task> </task>
<task id="LOCAL-00001" summary="Initial"> <task id="LOCAL-00001" summary="Initial">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -253,7 +211,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1773536678416</updated> <updated>1773536678416</updated>
</task> </task>
<option name="localTasksCounter" value="3" /> <task id="LOCAL-00003" summary="refactored, pre-buffer reuse">
<option name="closed" value="true" />
<created>1773581516834</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1773581516834</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -273,7 +239,8 @@
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value="Initial" /> <MESSAGE value="Initial" />
<MESSAGE value="pre-module refactor" /> <MESSAGE value="pre-module refactor" />
<option name="LAST_COMMIT_MESSAGE" value="pre-module refactor" /> <MESSAGE value="refactored, pre-buffer reuse" />
<option name="LAST_COMMIT_MESSAGE" value="refactored, pre-buffer reuse" />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />
+2 -1
View File
@@ -58,6 +58,7 @@
</annotationProcessorPaths> </annotationProcessorPaths>
</configuration> </configuration>
</plugin> </plugin>
<!--
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
@@ -82,13 +83,13 @@
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.relism.bench.Main</mainClass> <mainClass>dev.relism.bench.Main</mainClass>
</transformer> </transformer>
<!-- Required by JMH to merge service-loader files -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers> </transformers>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
-->
</plugins> </plugins>
</build> </build>
@@ -1,24 +0,0 @@
package dev.relism.bench;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
public class Main {
public static void main(String[] args) throws Exception {
if (args.length > 0 && args[0].equalsIgnoreCase("external")) {
ExternalBenchmark.main(args);
return;
}
System.out.println("Starting JMH Benchmarks... (Pass 'external' as arg to start standalone server)");
Options opts = new OptionsBuilder()
.include(FlashBenchmark.class.getSimpleName())
.resultFormat(ResultFormatType.TEXT)
.build();
new Runner(opts).run();
}
}
+24 -89
View File
@@ -13,32 +13,17 @@ import java.io.*;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Entry point for the Flash HTTP server. * Flash HTTP server. Owns a {@link GlobalRouter} with two routing tiers:
* * mounted sub-routers (matched by longest namespace prefix) and an internal
* <p> * router as fallback. Error handlers are scoped to their respective router.
* Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
* <ol>
* <li><b>Mounted sub-routers</b> — registered via {@link #mount}. Each owns a
* namespace
* prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest
* prefix first.</li>
* <li><b>Internal router</b> — the fallback used when no sub-router claims the
* path.
* Routes registered directly on the server ({@link #get}, {@link #post}, etc.)
* go here.</li>
* </ol>
*
* <p>
* Error handlers ({@link #onNotFound}, {@link #onException}) registered on the
* server
* apply only to the internal router. Each mounted sub-router has its own
* independent handlers.
*/ */
@Slf4j @Slf4j
public class HttpServer { public class HttpServer {
@@ -46,6 +31,7 @@ public class HttpServer {
private final ServerSocket serverSocket; private final ServerSocket serverSocket;
private final GlobalRouter globalRouter = new GlobalRouter(); private final GlobalRouter globalRouter = new GlobalRouter();
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
private volatile boolean stopped = false; private volatile boolean stopped = false;
private final CompletableFuture<Void> readyFuture = new CompletableFuture<>(); private final CompletableFuture<Void> readyFuture = new CompletableFuture<>();
@@ -69,11 +55,7 @@ public class HttpServer {
this.serverSocket = new ServerSocket(configuration.getPort()); this.serverSocket = new ServerSocket(configuration.getPort());
} }
/** /** Returns a future that completes once the accept loop is running and the server is ready. */
* Starts the server on a new non-daemon virtual thread and returns a future
* that completes
* when the accept loop is running and the server is ready to serve requests.
*/
public CompletableFuture<Void> start() { public CompletableFuture<Void> start() {
Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run); Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run);
return readyFuture; return readyFuture;
@@ -94,10 +76,7 @@ public class HttpServer {
} }
} }
/** /** Closes all active connections and shuts down the executor. Returns when complete. */
* Stops the server and waits for in-flight requests to complete.
* The returned future is already completed when this method returns.
*/
public CompletableFuture<Void> stop() { public CompletableFuture<Void> stop() {
stopped = true; stopped = true;
try { try {
@@ -105,6 +84,7 @@ public class HttpServer {
} catch (IOException e) { } catch (IOException e) {
log.error("Error closing server socket", e); log.error("Error closing server socket", e);
} }
activeSockets.forEach(s -> { try { s.close(); } catch (IOException ignored) {} });
executorService.shutdown(); executorService.shutdown();
try { try {
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) if (!executorService.awaitTermination(30, TimeUnit.SECONDS))
@@ -116,104 +96,66 @@ public class HttpServer {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
} }
// --- Routing ---
/** /**
* Mounts a sub-router under the given namespace prefix. * Mounts a sub-router under {@code namespace}. Requests whose path starts with the
* namespace are dispatched to {@code router}; longest prefix wins.
* *
* <p> * @throws dev.relism.exceptions.DuplicateNamespaceException if {@code namespace} is already mounted
* Any request whose path starts with {@code namespace} will be dispatched to
* {@code router}
* instead of the internal router. When multiple namespaces match, the longest
* prefix wins.
* The router's own {@code onNotFound} and {@code onException} handlers are used
* for its paths —
* the server-level handlers do not apply.
*
* @throws dev.relism.exceptions.DuplicateNamespaceException if
* {@code namespace}
* is already mounted
*/ */
public HttpServer mount(String namespace, AbstractRouter router) { public HttpServer mount(String namespace, AbstractRouter router) {
globalRouter.mount(namespace, router); globalRouter.mount(namespace, router);
return this; return this;
} }
// --- Global error handlers ---
/**
* Sets the 404 handler for routes registered directly on this server.
* Does not affect mounted sub-routers, which each carry their own not-found
* handler.
*/
public HttpServer onNotFound(SimpleHandler.FunctionalHandler handler) { public HttpServer onNotFound(SimpleHandler.FunctionalHandler handler) {
globalRouter.onNotFound(handler); globalRouter.onNotFound(handler);
return this; return this;
} }
/**
* Sets the exception handler for routes registered directly on this server.
* Does not affect mounted sub-routers, which each carry their own exception
* handler.
*/
public HttpServer onException(AbstractRouter.ExceptionHandler handler) { public HttpServer onException(AbstractRouter.ExceptionHandler handler) {
globalRouter.onException(handler); globalRouter.onException(handler);
return this; return this;
} }
// --- DSL ---
/**
* Registers an annotation-based handler on the internal router.
* The handler's class must carry a {@link dev.relism.routing.Route @Route}
* annotation
* declaring the HTTP method and path. To register on a specific sub-router,
* call
* {@link AbstractRouter#register} on that router directly.
*/
public HttpServer register(RequestHandler handler) { public HttpServer register(RequestHandler handler) {
globalRouter.register(handler); globalRouter.register(handler);
return this; return this;
} }
/** /**
* Registers a route on the internal router (not on any mounted sub-router). * Registers a route on the internal router. The handler return value drives the response:
* The handler's return value is used as the response body; returning a * return a {@link dev.relism.models.Response} to replace it entirely, any other non-null
* {@link dev.relism.models.Response} * value to set it as the body, or {@code null} to leave the response object unchanged.
* instance replaces the entire response object.
*/ */
public HttpServer get(String path, SimpleHandler.FunctionalHandler h) { public HttpServer get(String path, SimpleHandler.FunctionalHandler h) {
globalRouter.get(path, h); globalRouter.get(path, h);
return this; return this;
} }
/** @see #get(String, SimpleHandler.FunctionalHandler) */
public HttpServer post(String path, SimpleHandler.FunctionalHandler h) { public HttpServer post(String path, SimpleHandler.FunctionalHandler h) {
globalRouter.post(path, h); globalRouter.post(path, h);
return this; return this;
} }
/** @see #get(String, SimpleHandler.FunctionalHandler) */
public HttpServer put(String path, SimpleHandler.FunctionalHandler h) { public HttpServer put(String path, SimpleHandler.FunctionalHandler h) {
globalRouter.put(path, h); globalRouter.put(path, h);
return this; return this;
} }
/** @see #get(String, SimpleHandler.FunctionalHandler) */
public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) { public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) {
globalRouter.delete(path, h); globalRouter.delete(path, h);
return this; return this;
} }
// --- Request processing ---
private void process(Socket socket) { private void process(Socket socket) {
activeSockets.add(socket);
executorService.submit(() -> { executorService.submit(() -> {
try (socket; try (socket;
InputStream in = socket.getInputStream(); InputStream in = socket.getInputStream();
OutputStream out = new BufferedOutputStream(socket.getOutputStream())) { OutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
RequestParser parser = new RequestParser(configuration.getMaxHeaderBufferSize());
while (!stopped) { while (!stopped) {
Request request = RequestParser.parse(in); Request request = parser.parse(in);
if (request == null) if (request == null)
break; break;
@@ -236,7 +178,6 @@ public class HttpServer {
response.setBody(result); response.setBody(result);
} }
// Drain any unread body bytes so the stream is positioned at the next request.
request.getBody(); request.getBody();
out.write(HTTP_1_1); out.write(HTTP_1_1);
@@ -260,24 +201,18 @@ public class HttpServer {
} catch (IOException e) { } catch (IOException e) {
if (!stopped) if (!stopped)
log.error("I/O error handling request", e); log.error("I/O error handling request", e);
} finally {
activeSockets.remove(socket);
} }
}); });
} }
/**
* Returns true if the connection should be kept alive after this request.
* HTTP/1.1 default: keep-alive. HTTP/1.0 default: close.
* Explicit {@code Connection} header overrides the default.
*/
private static boolean isKeepAlive(Request request) { private static boolean isKeepAlive(Request request) {
String connection = request.getHeader("Connection"); if (request.headerEquals("Connection", "close")) return false;
if (connection != null) { // Detect HTTP version from last byte of protocol field: "HTTP/1.1" → '1', "HTTP/1.0" → '0'
return !"close".equalsIgnoreCase(connection);
}
// Detect HTTP version from last byte of protocol field: "HTTP/1.1" → '1',
// "HTTP/1.0" → '0'
ByteView protocol = request.getRequestLine().getProtocol(); ByteView protocol = request.getRequestLine().getProtocol();
return protocol.length() == 8 && protocol.byteAt(7) == '1'; return protocol.length() == 8 && protocol.byteAt(7) == '1'
|| request.headerEquals("Connection", "keep-alive");
} }
private static void writeStatusPhrase(OutputStream out, int statusCode) throws IOException { private static void writeStatusPhrase(OutputStream out, int statusCode) throws IOException {
@@ -8,4 +8,6 @@ import lombok.Data;
public class HttpServerConfiguration { public class HttpServerConfiguration {
private int port; private int port;
private String host; private String host;
@Builder.Default
private int maxHeaderBufferSize = 64 * 1024;
} }
@@ -10,25 +10,35 @@ import lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
/** /**
* Extreme Zero-Allocation Request Parser. * One instance per connection — the buffer is allocated once and reused across keep-alive
* Scans raw bytes to identify paths, protocols, and headers without intermediate String objects. * requests. Grows on demand (doubling, up to {@code maxHeaderBufferSize}). Zero String
* Uses buffered reading and direct byte comparisons for maximum performance. * allocations during parsing; paths, headers and protocol are exposed as {@link dev.relism.fpr.core.ByteView} slices.
*/ */
@Slf4j @Slf4j
public class RequestParser { public class RequestParser {
private static final int MAX_HEADER_SIZE = 8192; private static final int INITIAL_BUFFER_SIZE = 8192;
public static Request parse(InputStream in) throws IOException { private final int maxHeaderBufferSize;
byte[] buffer = new byte[MAX_HEADER_SIZE]; private byte[] buffer;
// 1. Robust read loop — accumulate until \r\n\r\n found or buffer full. public RequestParser() { this(64 * 1024); }
// prevTotal tracked per iteration so findEndOfHeader starts from max(0, prevTotal-3), public RequestParser(int maxHeaderBufferSize) {
// skipping already-scanned bytes. The -3 overlap catches \r\n\r\n split across two reads. this.maxHeaderBufferSize = maxHeaderBufferSize;
this.buffer = new byte[Math.min(INITIAL_BUFFER_SIZE, maxHeaderBufferSize)];
}
public Request parse(InputStream in) throws IOException {
int totalRead = 0; int totalRead = 0;
int headerEndIdx = -1; int headerEndIdx = -1;
while (totalRead < buffer.length) { while (true) {
if (totalRead == buffer.length) {
if (buffer.length >= maxHeaderBufferSize)
throw new IOException("Request headers exceed " + maxHeaderBufferSize + " bytes");
buffer = Arrays.copyOf(buffer, Math.min(buffer.length * 2, maxHeaderBufferSize));
}
int n = in.read(buffer, totalRead, buffer.length - totalRead); int n = in.read(buffer, totalRead, buffer.length - totalRead);
if (n <= 0) break; if (n <= 0) break;
int prevTotal = totalRead; int prevTotal = totalRead;
@@ -38,10 +48,9 @@ public class RequestParser {
} }
if (totalRead <= 0) return null; if (totalRead <= 0) return null;
if (headerEndIdx == -1) { if (headerEndIdx == -1) {
throw new IOException("Headers too large: buffer exhausted without finding \\r\\n\\r\\n"); throw new IOException("Request headers exceed " + maxHeaderBufferSize + " bytes");
} }
// 2. Scan Request Line: METHOD PATH PROTOCOL
int methodEnd = find(buffer, 0, headerEndIdx, (byte) ' '); int methodEnd = find(buffer, 0, headerEndIdx, (byte) ' ');
if (methodEnd == -1) throw new IOException("Invalid request line (method)"); if (methodEnd == -1) throw new IOException("Invalid request line (method)");
@@ -52,7 +61,6 @@ public class RequestParser {
int pathEnd = find(buffer, pathStart, headerEndIdx, (byte) ' '); int pathEnd = find(buffer, pathStart, headerEndIdx, (byte) ' ');
if (pathEnd == -1) throw new IOException("Invalid request line (path)"); if (pathEnd == -1) throw new IOException("Invalid request line (path)");
// Split path from query string at '?' — FPR only sees the clean path
int queryMark = find(buffer, pathStart, pathEnd, (byte) '?'); int queryMark = find(buffer, pathStart, pathEnd, (byte) '?');
FastPathViews.RequestByteView pathView = new FastPathViews.RequestByteView(buffer, pathStart, FastPathViews.RequestByteView pathView = new FastPathViews.RequestByteView(buffer, pathStart,
queryMark != -1 ? queryMark - pathStart : pathEnd - pathStart); queryMark != -1 ? queryMark - pathStart : pathEnd - pathStart);
@@ -66,7 +74,6 @@ public class RequestParser {
FastPathViews.RequestByteView protocolView = new FastPathViews.RequestByteView(buffer, protocolStart, protocolEnd - protocolStart); FastPathViews.RequestByteView protocolView = new FastPathViews.RequestByteView(buffer, protocolStart, protocolEnd - protocolStart);
// 3. Scan Headers — store raw offsets into buffer, zero objects allocated per header
HeaderMap headerMap = new HeaderMap(buffer); HeaderMap headerMap = new HeaderMap(buffer);
int current = find(buffer, protocolEnd, headerEndIdx, (byte) '\n') + 1; int current = find(buffer, protocolEnd, headerEndIdx, (byte) '\n') + 1;
int contentLength = 0; int contentLength = 0;
@@ -82,16 +89,13 @@ public class RequestParser {
headerMap.add(current, colon - current, valueStart, lineEnd - valueStart); headerMap.add(current, colon - current, valueStart, lineEnd - valueStart);
// Direct byte comparison to extract Content-Length without String allocation
if (equalsIgnoreCase(buffer, current, colon, "content-length")) { if (equalsIgnoreCase(buffer, current, colon, "content-length")) {
contentLength = parseInt(buffer, valueStart, lineEnd); contentLength = parseInt(buffer, valueStart, lineEnd);
} }
} }
current = lineEnd + 2; // skip \r\n current = lineEnd + 2;
} }
// Body is read lazily — only materialized if the handler calls req.getBody().
// Bytes already in the buffer past \r\n\r\n are handed off to LazyBody as read-ahead.
int bodyStart = headerEndIdx + 4; int bodyStart = headerEndIdx + 4;
int preBufLen = totalRead - bodyStart; int preBufLen = totalRead - bodyStart;
return Request.forParsed( return Request.forParsed(
@@ -8,8 +8,7 @@ import java.util.List;
/** /**
* Zero-allocation header storage backed by raw byte offsets into the shared request buffer. * Zero-allocation header storage backed by raw byte offsets into the shared request buffer.
* At parse time, only int offsets are recorded — zero objects allocated per header. * Offsets are recorded at parse time; Strings are allocated only on {@link #getFirst} / {@link #getAll}.
* String conversion is lazy: happens only when getFirst() or getAll() are called.
*/ */
public class HeaderMap { public class HeaderMap {
private static final int MAX_HEADERS = 32; private static final int MAX_HEADERS = 32;
@@ -23,7 +22,6 @@ public class HeaderMap {
this.buffer = buffer; this.buffer = buffer;
} }
/** Called at parse time. Stores raw offsets — zero allocations. */
public void add(int keyStart, int keyLen, int valStart, int valLen) { public void add(int keyStart, int keyLen, int valStart, int valLen) {
if (count >= MAX_HEADERS) throw new IllegalStateException("Too many headers: limit is " + MAX_HEADERS); if (count >= MAX_HEADERS) throw new IllegalStateException("Too many headers: limit is " + MAX_HEADERS);
keys[count * 2] = keyStart; keys[count * 2] = keyStart;
@@ -33,14 +31,12 @@ public class HeaderMap {
count++; count++;
} }
/** Lazy: decodes and returns the first matching value as a String. */
public String getFirst(String name) { public String getFirst(String name) {
int idx = indexOf(name); int idx = indexOf(name);
if (idx < 0) return null; if (idx < 0) return null;
return new String(buffer, values[idx * 2], values[idx * 2 + 1], StandardCharsets.UTF_8); return new String(buffer, values[idx * 2], values[idx * 2 + 1], StandardCharsets.UTF_8);
} }
/** Lazy: decodes and returns all matching values as a List<String>. */
public List<String> getAll(String name) { public List<String> getAll(String name) {
List<String> result = null; List<String> result = null;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -52,7 +48,6 @@ public class HeaderMap {
return result != null ? result : List.of(); return result != null ? result : List.of();
} }
/** Lazy: decodes and returns all values as a List<String>. */
public List<String> getAll() { public List<String> getAll() {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -61,7 +56,21 @@ public class HeaderMap {
return result; return result;
} }
/** Zero-copy: returns a ByteView over the raw value bytes without allocating a String. */ public boolean headerValueEqualsIgnoreCase(String name, String value) {
int idx = indexOf(name);
if (idx < 0) return false;
int vs = values[idx * 2], vl = values[idx * 2 + 1];
if (vl != value.length()) return false;
for (int i = 0; i < vl; i++) {
byte b = buffer[vs + i];
if (b >= 'A' && b <= 'Z') b += 32;
char c = value.charAt(i);
if (c >= 'A' && c <= 'Z') c += 32;
if (b != (byte) c) return false;
}
return true;
}
public ByteView getView(String name) { public ByteView getView(String name) {
int idx = indexOf(name); int idx = indexOf(name);
if (idx < 0) return null; if (idx < 0) return null;
@@ -80,7 +89,6 @@ public class HeaderMap {
return -1; return -1;
} }
/** Case-insensitive comparison between a buffer slice and a String. Zero allocations. */
private boolean keyMatches(int i, String name) { private boolean keyMatches(int i, String name) {
int ks = keys[i * 2], kl = keys[i * 2 + 1]; int ks = keys[i * 2], kl = keys[i * 2 + 1];
if (kl != name.length()) return false; if (kl != name.length()) return false;
@@ -5,14 +5,8 @@ import java.io.InputStream;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
/** /**
* Deferred request body reader. * Deferred body reader. On the first {@link #get()} call, pre-buffered bytes from the header
* * read-ahead are used first, then the remainder is pulled from the socket stream. Result is cached.
* <p>On the first call to {@link #get()}, the body is materialized: any bytes already
* buffered from the 8 KB header read-ahead are copied first, then the remainder is pulled
* from the socket stream. The result is cached so subsequent calls are free.
*
* <p>When {@code contentLength} is zero the instance is pre-resolved to an empty array
* and no stream access ever occurs.
*/ */
final class LazyBody { final class LazyBody {
private static final byte[] EMPTY = new byte[0]; private static final byte[] EMPTY = new byte[0];
@@ -32,14 +26,12 @@ final class LazyBody {
this.preBufLen = preBufLen; this.preBufLen = preBufLen;
} }
/** Returns a pre-resolved {@code LazyBody} backed by an already-materialized byte array. */
static LazyBody of(byte[] bytes) { static LazyBody of(byte[] bytes) {
LazyBody lb = new LazyBody(null, bytes.length, null, 0, 0); LazyBody lb = new LazyBody(null, bytes.length, null, 0, 0);
lb.resolved = bytes; lb.resolved = bytes;
return lb; return lb;
} }
/** Returns a pre-resolved empty {@code LazyBody}. */
static LazyBody empty() { static LazyBody empty() {
LazyBody lb = new LazyBody(null, 0, null, 0, 0); LazyBody lb = new LazyBody(null, 0, null, 0, 0);
lb.resolved = EMPTY; lb.resolved = EMPTY;
@@ -5,11 +5,8 @@ import dev.relism.fpr.core.ByteView;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* Path parameters captured during routing. * Path parameters captured during routing, stored as byte offsets into the path view.
* * {@link #get} allocates a String on call; {@link #view} is zero-copy.
* <p>Values are stored as byte offsets into the original path view. String conversion
* is lazy and happens only on {@link #get}; {@link #view} is zero-copy.
* Lookup is a linear scan — param counts are always small (typically 13).
*/ */
public class PathParams { public class PathParams {
private final ByteView source; private final ByteView source;
@@ -24,7 +21,6 @@ public class PathParams {
this.lens = lens; this.lens = lens;
} }
/** Lazy — allocates a {@code String} only on call. {@code null} if the param is absent. */
public String get(String name) { public String get(String name) {
int i = indexOf(name); int i = indexOf(name);
if (i < 0) return null; if (i < 0) return null;
@@ -33,7 +29,6 @@ public class PathParams {
return new String(bytes, StandardCharsets.UTF_8); return new String(bytes, StandardCharsets.UTF_8);
} }
/** Zero-copy — returns a {@link ByteView} slice over the raw path bytes. */
ByteView view(String name) { ByteView view(String name) {
int i = indexOf(name); int i = indexOf(name);
if (i < 0) return null; if (i < 0) return null;
@@ -7,14 +7,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Lazy access to URL query parameters ({@code ?key=value&...}). * Lazy query parameter access ({@code ?key=value&...}). Backed by a zero-copy {@link ByteView}
* Backed by a zero-copy {@link ByteView} over the raw query string bytes from the request buffer. * over the raw query string bytes — no parsing at construction, values decoded on demand.
* No parsing happens at construction — values are decoded on demand.
*
* <p>{@link #view} is zero-copy; {@link #get} and {@link #getAll} allocate only the result String(s).
*/ */
public class QueryParams { public class QueryParams {
/** Singleton returned when the request has no query string. All methods return empty/null. */
public static final QueryParams EMPTY = new QueryParams(null); public static final QueryParams EMPTY = new QueryParams(null);
private final ByteView raw; private final ByteView raw;
@@ -23,7 +19,6 @@ public class QueryParams {
this.raw = raw; this.raw = raw;
} }
/** Lazy — decodes the first value for {@code name}, or {@code null} if absent. */
public String get(String name) { public String get(String name) {
long r = findFirst(name); long r = findFirst(name);
if (r < 0) return null; if (r < 0) return null;
@@ -33,7 +28,6 @@ public class QueryParams {
return new String(bytes, StandardCharsets.UTF_8); return new String(bytes, StandardCharsets.UTF_8);
} }
/** Zero-copy — returns a {@link ByteView} slice over the raw value bytes. */
ByteView view(String name) { ByteView view(String name) {
long r = findFirst(name); long r = findFirst(name);
if (r < 0) return null; if (r < 0) return null;
@@ -44,7 +38,6 @@ public class QueryParams {
}; };
} }
/** Lazy — returns all values for {@code name}, or an empty list if absent. */
public List<String> getAll(String name) { public List<String> getAll(String name) {
if (raw == null) return List.of(); if (raw == null) return List.of();
List<String> result = null; List<String> result = null;
@@ -70,10 +63,7 @@ public class QueryParams {
return result != null ? result : List.of(); return result != null ? result : List.of();
} }
/** /** Returns (valStart << 32) | valLen, or -1 if not found. */
* Scans for the first occurrence of {@code name=value}.
* Returns {@code (valStart << 32) | valLen}, or {@code -1} if not found. Zero allocations.
*/
private long findFirst(String name) { private long findFirst(String name) {
if (raw == null) return -1L; if (raw == null) return -1L;
int i = 0, len = raw.length(); int i = 0, len = raw.length();
@@ -31,17 +31,10 @@ public class Request {
this.queryParams = null; this.queryParams = null;
} }
/** Convenience constructor for test mocks and pre-materialized bodies. */
public Request(RequestLine requestLine, byte[] body) { public Request(RequestLine requestLine, byte[] body) {
this(requestLine, LazyBody.of(body)); this(requestLine, LazyBody.of(body));
} }
/**
* Factory used by {@link dev.relism.RequestParser}: creates a request whose body is read
* from {@code stream} on the first call to {@link #getBody()}.
*
* @param preBufLen bytes already buffered past the header end (from the 8 KB read-ahead)
*/
public static Request forParsed(RequestLine requestLine, InputStream stream, public static Request forParsed(RequestLine requestLine, InputStream stream,
int contentLength, byte[] headerBuf, int contentLength, byte[] headerBuf,
int bodyStart, int preBufLen) { int bodyStart, int preBufLen) {
@@ -51,30 +44,21 @@ public class Request {
return new Request(requestLine, lazy); return new Request(requestLine, lazy);
} }
/** Materializes and returns the request body, reading from the socket if not yet done. */
public byte[] getBody() { return lazyBody.get(); } public byte[] getBody() { return lazyBody.get(); }
// --- Header access (lazy: String allocated only on call) ---
public String getHeader(String name) { return requestLine.getHeaders().getFirst(name); } public String getHeader(String name) { return requestLine.getHeaders().getFirst(name); }
public List<String> getHeaders(String name) { return requestLine.getHeaders().getAll(name); } public List<String> getHeaders(String name) { return requestLine.getHeaders().getAll(name); }
public List<String> getHeaders() { return requestLine.getHeaders().getAll(); } public List<String> getHeaders() { return requestLine.getHeaders().getAll(); }
public boolean headerEquals(String name, String value) { return requestLine.getHeaders().headerValueEqualsIgnoreCase(name, value); }
// --- Path param access ---
/** Lazy: decodes the named path parameter to a {@code String}. */
public String getPathParam(String name) { public String getPathParam(String name) {
return pathParams != null ? pathParams.get(name) : null; return pathParams != null ? pathParams.get(name) : null;
} }
// --- Query param access ---
/** Lazy: decodes the first value of {@code name}, or {@code null} if absent. */
public String getQueryParam(String name) { public String getQueryParam(String name) {
return resolveQueryParams().get(name); return resolveQueryParams().get(name);
} }
/** Lazy: decodes all values of {@code name} (e.g. {@code ?tag=a&tag=b}). */
public List<String> getQueryParams(String name) { public List<String> getQueryParams(String name) {
return resolveQueryParams().getAll(name); return resolveQueryParams().getAll(name);
} }
@@ -12,17 +12,9 @@ import lombok.Getter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* Base class for all routers, used both for the server's internal router and for * Base router. Each router has a namespace prefix (default {@code "/"}); routes added via
* mounted sub-routers. * {@link #get}, {@link #post}, etc. are relative to it. Error handlers are scoped to this router.
* * The namespace is set automatically by {@link GlobalRouter#mount}.
* <p>Each router has a <b>namespace</b> (e.g. {@code /api}). Routes added via
* {@link #get}, {@link #post}, etc. are <em>relative</em> to the namespace; the
* implementation prepends it when registering. The namespace is {@code "/"} by default
* and is set automatically by {@link GlobalRouter#mount}.
*
* <p>Error handlers ({@link #onNotFound}, {@link #onException}) are scoped to this router.
* When a sub-router is mounted on the server, its handlers take precedence over the
* server-level ones for all paths under its namespace.
*/ */
public abstract class AbstractRouter { public abstract class AbstractRouter {
@@ -32,8 +24,6 @@ public abstract class AbstractRouter {
@Getter(AccessLevel.PACKAGE) @Getter(AccessLevel.PACKAGE)
protected byte[] namespaceBytes = new byte[]{ '/' }; protected byte[] namespaceBytes = new byte[]{ '/' };
// --- Default handlers ---
protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> { protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> {
res.setStatusCode(404); res.setStatusCode(404);
res.setContentType(ContentType.TEXT_HTML); res.setContentType(ContentType.TEXT_HTML);
@@ -46,7 +36,6 @@ public abstract class AbstractRouter {
return ErrorPages.renderException(req, ex); return ErrorPages.renderException(req, ex);
}; };
// Package-private — used by GlobalRouter only
SimpleHandler getNotFoundHandler() { return notFoundHandler; } SimpleHandler getNotFoundHandler() { return notFoundHandler; }
ExceptionHandler getExceptionHandler() { return exceptionHandler; } ExceptionHandler getExceptionHandler() { return exceptionHandler; }
void setNamespace(String namespace) { void setNamespace(String namespace) {
@@ -54,54 +43,37 @@ public abstract class AbstractRouter {
this.namespaceBytes = namespace.getBytes(StandardCharsets.UTF_8); this.namespaceBytes = namespace.getBytes(StandardCharsets.UTF_8);
} }
// --- Public API ---
/** Overrides the default 404 response for unmatched paths under this router's namespace. */
public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) { public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) {
this.notFoundHandler = new SimpleHandler(handler); this.notFoundHandler = new SimpleHandler(handler);
return this; return this;
} }
/** Overrides the default 500 response for uncaught exceptions thrown by handlers under this router. */
public AbstractRouter onException(ExceptionHandler handler) { public AbstractRouter onException(ExceptionHandler handler) {
this.exceptionHandler = handler; this.exceptionHandler = handler;
return this; return this;
} }
/** /**
* Registers a route relative to this router's namespace. * Registers a route relative to this router's namespace. Return a {@link Response} to replace
* * it entirely, any other non-null value to set it as the body, or {@code null} to leave it unchanged.
* <p>The handler's return value drives the response:
* <ul>
* <li>Return a {@link Response} to replace the entire response object.</li>
* <li>Return any other non-null value to use it as the body (via {@code toString()} or raw bytes).</li>
* <li>Return {@code null} to leave the response unchanged from what was set on the {@code Response} parameter.</li>
* </ul>
*/ */
public AbstractRouter get(String path, SimpleHandler.FunctionalHandler handler) { public AbstractRouter get(String path, SimpleHandler.FunctionalHandler handler) {
return addRoute(HttpMethod.GET, PathUtils.sanitize(path), new SimpleHandler(handler)); return addRoute(HttpMethod.GET, PathUtils.sanitize(path), new SimpleHandler(handler));
} }
/** @see #get(String, SimpleHandler.FunctionalHandler) */
public AbstractRouter post(String path, SimpleHandler.FunctionalHandler handler) { public AbstractRouter post(String path, SimpleHandler.FunctionalHandler handler) {
return addRoute(HttpMethod.POST, PathUtils.sanitize(path), new SimpleHandler(handler)); return addRoute(HttpMethod.POST, PathUtils.sanitize(path), new SimpleHandler(handler));
} }
/** @see #get(String, SimpleHandler.FunctionalHandler) */
public AbstractRouter put(String path, SimpleHandler.FunctionalHandler handler) { public AbstractRouter put(String path, SimpleHandler.FunctionalHandler handler) {
return addRoute(HttpMethod.PUT, PathUtils.sanitize(path), new SimpleHandler(handler)); return addRoute(HttpMethod.PUT, PathUtils.sanitize(path), new SimpleHandler(handler));
} }
/** @see #get(String, SimpleHandler.FunctionalHandler) */
public AbstractRouter delete(String path, SimpleHandler.FunctionalHandler handler) { public AbstractRouter delete(String path, SimpleHandler.FunctionalHandler handler) {
return addRoute(HttpMethod.DELETE, PathUtils.sanitize(path), new SimpleHandler(handler)); return addRoute(HttpMethod.DELETE, PathUtils.sanitize(path), new SimpleHandler(handler));
} }
/** /** Registers a class-based handler; the class must be annotated with {@link Route @Route}. */
* Registers a class-based handler. The handler's class must be annotated with
* {@link Route @Route} declaring the HTTP method and path (relative to this router's namespace).
* If the annotation is absent, the call is silently ignored.
*/
public AbstractRouter register(RequestHandler handler) { public AbstractRouter register(RequestHandler handler) {
Route annotation = handler.getClass().getAnnotation(Route.class); Route annotation = handler.getClass().getAnnotation(Route.class);
if (annotation != null) { if (annotation != null) {
@@ -110,18 +82,10 @@ public abstract class AbstractRouter {
return this; return this;
} }
// --- For implementors ---
public abstract RequestHandler route(Request request); public abstract RequestHandler route(Request request);
protected abstract AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler); protected abstract AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler);
/**
* Sets the path parameters captured during routing.
* Implementations must call this whenever the matched route contains path parameters.
* Centralised here so all router implementations participate in the same contract
* and produce a consistent {@link PathParams} regardless of the matching strategy.
*/
protected static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) { protected static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) {
request.setPathParams(new PathParams(source, names, starts, lens)); request.setPathParams(new PathParams(source, names, starts, lens));
} }
@@ -14,16 +14,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Top-level dispatcher owned by {@link dev.relism.HttpServer}. * Top-level dispatcher. Routes to the longest-matching mounted sub-router first,
* * falling back to the internal {@link FastPathRouterImpl}.
* <p>Routing order on each request:
* <ol>
* <li>Iterates mounted sub-routers in descending namespace length order (longest prefix first)
* and delegates to the first one whose namespace is a prefix of the request path.</li>
* <li>If no sub-router matches, falls through to the internal {@link FastPathRouterImpl}.</li>
* </ol>
*
* <p>Not intended to be instantiated or subclassed directly — use {@link dev.relism.HttpServer}.
*/ */
public class GlobalRouter extends AbstractRouter { public class GlobalRouter extends AbstractRouter {
private final Map<String, AbstractRouter> subRoutersMap = new HashMap<>(); private final Map<String, AbstractRouter> subRoutersMap = new HashMap<>();
@@ -53,7 +45,6 @@ public class GlobalRouter extends AbstractRouter {
return h != null ? h : notFoundHandler; return h != null ? h : notFoundHandler;
} }
/** Resolves the scoped exception handler for the router that owns the request path. */
public ExceptionHandler resolveExceptionHandler(Request request) { public ExceptionHandler resolveExceptionHandler(Request request) {
ByteView path = request.getRequestLine().getPath(); ByteView path = request.getRequestLine().getPath();
for (AbstractRouter sub : sortedSubRouters) { for (AbstractRouter sub : sortedSubRouters) {
@@ -12,30 +12,16 @@ import dev.relism.routing.AbstractRouter;
import dev.relism.routing.PathUtils; import dev.relism.routing.PathUtils;
/** /**
* Router implementation backed by the {@code fpr-core} byte-level state machine. * Router backed by the {@code fpr-core} byte-level state machine. Routes are compiled lazily
* * on the first request and recompiled when routes are added after startup. Matching runs on a
* <p>Routes are compiled lazily: the internal {@code FastPathRouter} is built on the first * virtual {@code METHOD + path} byte sequence in a single pass; {@link MatchResult} and
* incoming request after one or more routes have been added. Adding a route after the server * {@link FastPathViews.MethodPathByteView} are reused per-thread to avoid hot-path allocations.
* has started is safe — the router is marked dirty and recompiled on the next request.
*
* <p>Matching is done on a virtual {@code METHOD + path} byte sequence to avoid a two-step
* lookup. Captured path parameters are exposed via {@link dev.relism.models.PathParams} on
* the request object, accessible through {@link dev.relism.models.Request#getPathParams()}.
*
* <p>Thread safety: {@code FastPathRouter.match()} is safe for concurrent calls as of
* {@code fpr-core} 1.1.0 — traversal state ({@code RouteSearch}, {@code SegmentCursor}) is
* held in method-local variables, making the compiled router instance fully immutable.
*/ */
public class FastPathRouterImpl extends AbstractRouter { public class FastPathRouterImpl extends AbstractRouter {
private final RouterBuilder<RequestHandler> builder = new RouterBuilder<>(); private final RouterBuilder<RequestHandler> builder = new RouterBuilder<>();
private volatile FastPathRouter<ByteView, RequestHandler> router; private volatile FastPathRouter<ByteView, RequestHandler> router;
private String[] cachedParamNames; private String[] cachedParamNames;
/**
* Thread-local holders for per-request reusable objects.
* Both {@link MatchResult} and {@link FastPathViews.MethodPathByteView} are reset before use,
* so no allocation occurs on the routing hot path.
*/
private static final class FastPathRouterContext { private static final class FastPathRouterContext {
private static final ThreadLocal<MatchResult<RequestHandler>> RESULT_HOLDER = private static final ThreadLocal<MatchResult<RequestHandler>> RESULT_HOLDER =
ThreadLocal.withInitial(() -> new MatchResult<>(32, 128)); ThreadLocal.withInitial(() -> new MatchResult<>(32, 128));
@@ -55,7 +41,7 @@ public class FastPathRouterImpl extends AbstractRouter {
protected AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler) { protected AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler) {
String fullPath = PathUtils.join(namespace, path); String fullPath = PathUtils.join(namespace, path);
builder.add(StringRouteParser.parse(method.name() + fullPath), handler); builder.add(StringRouteParser.parse(method.name() + fullPath), handler);
this.router = null; // Mark dirty this.router = null;
return this; return this;
} }
@@ -77,7 +63,6 @@ public class FastPathRouterImpl extends AbstractRouter {
return null; return null;
} }
// CAPTURE PARAMETERS
int count = result.paramCount(); int count = result.paramCount();
if (count > 0) { if (count > 0) {
int methodLen = method.getBytes().length; int methodLen = method.getBytes().length;
@@ -97,10 +82,6 @@ public class FastPathRouterImpl extends AbstractRouter {
return result.handler(); return result.handler();
} }
/**
* Ensures the FPR state machine is compiled. Uses double-checked locking on a {@code volatile}
* field so the fast path (already compiled) is a single null-check with no synchronization.
*/
private void ensureCompiled() { private void ensureCompiled() {
if (router == null) { if (router == null) {
synchronized (this) { synchronized (this) {
@@ -5,17 +5,10 @@ import lombok.NoArgsConstructor;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /** {@link dev.relism.fpr.core.ByteView} implementations used on the router and parser hot paths. */
* Container class for various {@link ByteView} implementations used by the FastPathRouter.
* Consolidating these views reduces package clutter while maintaining high-performance byte access.
*/
@NoArgsConstructor @NoArgsConstructor
public final class FastPathViews { public final class FastPathViews {
/**
* High-performance, zero-copy ByteView that points to a shared request buffer.
* Used by the {@link dev.relism.RequestParser} to scan for paths without allocations.
*/
public static final class RequestByteView implements ByteView { public static final class RequestByteView implements ByteView {
private final byte[] buffer; private final byte[] buffer;
private final int start; private final int start;
@@ -41,24 +34,17 @@ public final class FastPathViews {
} }
@Override @Override
public String toString() { // CHANGED: decode slice directly; avoids full-buffer String copy public String toString() {
return new String(buffer, start, length, StandardCharsets.UTF_8); return new String(buffer, start, length, StandardCharsets.UTF_8);
} }
} }
/** /** Mutable composite view: method bytes + path. Reused via ThreadLocal — call reset() before use. */
* A composite ByteView that prefixes a method's bytes to a path view.
* Enables method+path routing in a single pass with no allocation.
*
* <p>Mutable by design — reused across requests via {@code ThreadLocal}.
* Call {@link #reset} before each use.
*/
public static final class MethodPathByteView implements ByteView { public static final class MethodPathByteView implements ByteView {
private byte[] method; private byte[] method;
private ByteView path; private ByteView path;
private int totalLength; private int totalLength;
/** Binds this view to a new method+path pair. Must be called before each use. */
public void reset(byte[] method, ByteView path) { public void reset(byte[] method, ByteView path) {
this.method = method; this.method = method;
this.path = path; this.path = path;
@@ -76,9 +62,6 @@ public final class FastPathViews {
} }
} }
/**
* ByteView implementation for raw byte arrays, typically from a socket.
*/
public static class SocketByteView implements ByteView { public static class SocketByteView implements ByteView {
private final byte[] data; private final byte[] data;
@@ -97,9 +80,6 @@ public final class FastPathViews {
} }
} }
/**
* ByteView implementation for Java Strings.
*/
public static class StringByteView implements ByteView { public static class StringByteView implements ByteView {
private final byte[] bytes; private final byte[] bytes;
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.ServerSocket; import java.net.ServerSocket;
@@ -62,17 +63,32 @@ class HttpServerTest {
try (Socket socket = new Socket("127.0.0.1", port); try (Socket socket = new Socket("127.0.0.1", port);
OutputStream out = socket.getOutputStream(); OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) { InputStream in = socket.getInputStream()) {
out.write(rawHttp.getBytes(StandardCharsets.UTF_8)); out.write(rawHttp.getBytes(StandardCharsets.UTF_8));
out.flush(); out.flush();
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); // Read until \r\n\r\n to get the full header block
byte[] buffer = new byte[8192]; ByteArrayOutputStream headerBuf = new ByteArrayOutputStream();
int read; int b, prev3 = -1, prev2 = -1, prev1 = -1;
while ((read = in.read(buffer)) != -1) { while ((b = in.read()) != -1) {
baos.write(buffer, 0, read); headerBuf.write(b);
if (prev3 == '\r' && prev2 == '\n' && prev1 == '\r' && b == '\n') break;
prev3 = prev2; prev2 = prev1; prev1 = b;
} }
return baos.toString(StandardCharsets.UTF_8); String headers = headerBuf.toString(StandardCharsets.UTF_8);
// Parse Content-Length
int contentLength = 0;
for (String line : headers.split("\r\n")) {
if (line.toLowerCase().startsWith("content-length:")) {
contentLength = Integer.parseInt(line.substring(line.indexOf(':') + 1).trim());
break;
}
}
// Read exactly Content-Length bytes for the body
byte[] body = in.readNBytes(contentLength);
return headers + new String(body, StandardCharsets.UTF_8);
} }
} }
@@ -16,7 +16,7 @@ class RequestParserTest {
private static Request parse(String raw) throws IOException { private static Request parse(String raw) throws IOException {
byte[] bytes = raw.replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8); byte[] bytes = raw.replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8);
return RequestParser.parse(new ByteArrayInputStream(bytes)); return new RequestParser().parse(new ByteArrayInputStream(bytes));
} }
private static String req(String requestLine, String... headers) { private static String req(String requestLine, String... headers) {
@@ -70,7 +70,7 @@ class RequestParserTest {
void body_parsed() throws IOException { void body_parsed() throws IOException {
String body = "hello body"; String body = "hello body";
String raw = "POST / HTTP/1.1\r\nContent-Length: " + body.length() + "\r\n\r\n" + body; String raw = "POST / HTTP/1.1\r\nContent-Length: " + body.length() + "\r\n\r\n" + body;
Request r = RequestParser.parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8))); Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
assertNotNull(r); assertNotNull(r);
assertEquals(body, new String(r.getBody(), StandardCharsets.UTF_8)); assertEquals(body, new String(r.getBody(), StandardCharsets.UTF_8));
} }
@@ -85,14 +85,14 @@ class RequestParserTest {
@Test @Test
void emptyInputStream_returnsNull() throws IOException { void emptyInputStream_returnsNull() throws IOException {
assertNull(RequestParser.parse(new ByteArrayInputStream(new byte[0]))); assertNull(new RequestParser().parse(new ByteArrayInputStream(new byte[0])));
} }
@Test @Test
void missingHeaderTerminator_throwsIOException() { void missingHeaderTerminator_throwsIOException() {
// Valid request line but stream ends before \r\n\r\n // Valid request line but stream ends before \r\n\r\n
byte[] raw = "GET / HTTP/1.1\r\nHost: localhost\r\n".getBytes(StandardCharsets.UTF_8); byte[] raw = "GET / HTTP/1.1\r\nHost: localhost\r\n".getBytes(StandardCharsets.UTF_8);
assertThrows(IOException.class, () -> RequestParser.parse(new ByteArrayInputStream(raw))); assertThrows(IOException.class, () -> new RequestParser().parse(new ByteArrayInputStream(raw)));
} }
@Test @Test
@@ -107,11 +107,12 @@ class RequestParserTest {
} }
@Test @Test
void headersOverBufferSize_throwsIOException() { void headers_exceedingMaxBufferSize_throwsIOException() {
// 9 KB of data with no \r\n\r\n exhausts the 8 KB buffer // Feed more bytes than the configured cap with no \r\n\r\n — must throw
byte[] giant = new byte[9000]; int cap = 16 * 1024;
byte[] giant = new byte[cap + 1];
Arrays.fill(giant, (byte) 'A'); Arrays.fill(giant, (byte) 'A');
assertThrows(IOException.class, () -> RequestParser.parse(new ByteArrayInputStream(giant))); assertThrows(IOException.class, () -> new RequestParser(cap).parse(new ByteArrayInputStream(giant)));
} }
@Test @Test
@@ -119,7 +120,7 @@ class RequestParserTest {
// Content-Length claims 50 but stream ends after 5 bytes // Content-Length claims 50 but stream ends after 5 bytes
String body = "hello"; String body = "hello";
String raw = "POST / HTTP/1.1\r\nContent-Length: 50\r\n\r\n" + body; String raw = "POST / HTTP/1.1\r\nContent-Length: 50\r\n\r\n" + body;
Request r = RequestParser.parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8))); Request r = new RequestParser().parse(new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)));
assertNotNull(r); assertNotNull(r);
assertEquals(50, r.getBody().length); assertEquals(50, r.getBody().length);
assertEquals(body, new String(r.getBody(), 0, body.length(), StandardCharsets.UTF_8)); assertEquals(body, new String(r.getBody(), 0, body.length(), StandardCharsets.UTF_8));