optimized dynamic body size impl, decluttering javadocs/comments
This commit is contained in:
Generated
+73
-106
@@ -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[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"Application.ExternalBenchmark.executor": "Run",
|
"Application.ExternalBenchmark.executor": "Run",
|
||||||
"Application.Main.executor": "Run",
|
"Application.Main.executor": "Run",
|
||||||
"Application.dev.relism.bench.Main.executor": "Run",
|
"Application.dev.relism.bench.Main.executor": "Run",
|
||||||
"JUnit.RequestParserTest.executor": "Run",
|
"JUnit.RequestParserTest.executor": "Run",
|
||||||
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
||||||
"Maven.FlashPractice [test].executor": "Run",
|
"Maven.FlashPractice [test].executor": "Run",
|
||||||
"Maven.flash [compile].executor": "Run",
|
"Maven.flash [compile].executor": "Run",
|
||||||
"Maven.flash [install].executor": "Run",
|
"Maven.flash [install].executor": "Run",
|
||||||
"Maven.flash-bench [clean].executor": "Run",
|
"Maven.flash [test].executor": "Run",
|
||||||
"Maven.flash-bench [install].executor": "Run",
|
"Maven.flash-bench [clean].executor": "Run",
|
||||||
"Maven.flash-bench [validate].executor": "Run",
|
"Maven.flash-bench [install].executor": "Run",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"Maven.flash-bench [validate].executor": "Run",
|
||||||
"RunOnceActivity.MCP Project settings loaded": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "master",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"ignore.virus.scanning.warn.message": "true",
|
"git-widget-placeholder": "master",
|
||||||
"kotlin-language-version-configured": "true",
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
"kotlin-language-version-configured": "true",
|
||||||
"node.js.detected.package.eslint": "true",
|
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java",
|
"nodejs_package_manager_path": "npm",
|
||||||
"project.structure.last.edited": "Modules",
|
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java",
|
||||||
"project.structure.proportion": "0.15",
|
"project.structure.last.edited": "Modules",
|
||||||
"project.structure.side.proportion": "0.1150748",
|
"project.structure.proportion": "0.15",
|
||||||
"settings.editor.selected.configurable": "ml.llm.LLMThirdPartyAiProvidersConfigurable",
|
"project.structure.side.proportion": "0.1150748",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"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
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 1–3).
|
|
||||||
*/
|
*/
|
||||||
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) {
|
||||||
|
|||||||
+5
-24
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user