Repository: javalin/javalin
Pull request #2369 (2025-02-09 10:36:23)

[http] Add config option for response buffer size with a new default
As discussed on Discord:
This PR adds a config option for the response buffer size that used to be the [Kotlin default of 8kb](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/src/kotlin/io/Constants.kt#L13) and was set explicitly to 4096.
The new default is the same what Jetty has by default.

Link to the small discussion on Discord: https://discord.com/channels/804862058528505896/805106384977920040/1336426384981102663

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/javalin/javalin/pull/2369?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javalin) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 87.13%. Comparing base [(`2b63b76`)](https://app.codecov.io/gh/javalin/javalin/commit/2b63b76fd4b262c4eb8ff5187926e83bd2dd6129?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javalin) to head [(`6cc5948`)](https://app.codecov.io/gh/javalin/javalin/commit/6cc5948b7b9476309cc8faa6c166412de243c78f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javalin).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##             master    #2369   +/-   ##
=========================================
  Coverage     87.12%   87.13%           
  Complexity     1320     1320           
=========================================
  Files           148      148           
  Lines          4467     4468    +1     
  Branches        512      512           
=========================================
+ Hits           3892     3893    +1     
  Misses          354      354           
  Partials        221      221           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/javalin/javalin/pull/2369?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javalin).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javalin).

tipsy: Thanks @Playacem, and thanks @dzikoysk for reviewing !

Patch:
diff --git a/javalin/src/main/java/io/javalin/config/HttpConfig.kt b/javalin/src/main/java/io/javalin/config/HttpConfig.kt
index 09cc72c1..efbde710 100644
--- a/javalin/src/main/java/io/javalin/config/HttpConfig.kt
+++ b/javalin/src/main/java/io/javalin/config/HttpConfig.kt
@@ -17,6 +17,7 @@ class HttpConfig(private val cfg: JavalinConfig) {
     @JvmField var prefer405over404 = false
     @JvmField var strictContentTypes = false
     @JvmField var maxRequestSize = 1_000_000L // increase this or use inputstream to handle large requests
+    @JvmField var responseBufferSize: Int? = null
     @JvmField var defaultContentType = ContentType.PLAIN
     @JvmField var asyncTimeout = 0L
     //@formatter:on
diff --git a/javalin/src/main/java/io/javalin/http/servlet/JavalinServlet.kt b/javalin/src/main/java/io/javalin/http/servlet/JavalinServlet.kt
index 2889b6e3..a52d7d99 100644
--- a/javalin/src/main/java/io/javalin/http/servlet/JavalinServlet.kt
+++ b/javalin/src/main/java/io/javalin/http/servlet/JavalinServlet.kt
@@ -109,7 +109,7 @@ class JavalinServlet(val cfg: JavalinConfig) : HttpServlet() {
             if (responseWritten.getAndSet(true)) return // prevent writing more than once, it's required because timeout listener can terminate the flow at any time
             resultInputStream()?.use { resultStream ->
                 val etagWritten = ETagGenerator.tryWriteEtagAndClose(cfg.http.generateEtags, this, resultStream)
-                if (!etagWritten) resultStream.copyTo(outputStream(), 4096)
+                if (!etagWritten) resultStream.copyTo(outputStream(), cfg.http.responseBufferSize ?: 32_768) // default should never happen, we add a fallback just in case
             }
             cfg.pvt.requestLogger?.handle(this, executionTimeMs())
         } catch (throwable: Throwable) {
diff --git a/javalin/src/main/java/io/javalin/jetty/JettyServer.kt b/javalin/src/main/java/io/javalin/jetty/JettyServer.kt
index ad9283b3..fd203dcb 100644
--- a/javalin/src/main/java/io/javalin/jetty/JettyServer.kt
+++ b/javalin/src/main/java/io/javalin/jetty/JettyServer.kt
@@ -79,6 +79,10 @@ class JettyServer(private val cfg: JavalinConfig) {
             })
             val httpConfiguration = defaultHttpConfiguration()
             cfg.pvt.jetty.httpConfigurationConfigs.forEach { it.accept(httpConfiguration) } // apply user config (before connectors)
+            // use the jetty value, either the default or something the user has specified with the cfg.jetty.modifyHttpConfiguration option if there is no value set with the new api.
+            if (cfg.http.responseBufferSize == null) {
+                cfg.http.responseBufferSize = httpConfiguration.outputBufferSize
+            }
             cfg.pvt.jetty.connectors.map { it.apply(this, httpConfiguration) }.forEach(this::addConnector) // add user connectors
             if (connectors.isEmpty()) { // add default connector if no connectors are specified
                 connectors = arrayOf(ServerConnector(server, HttpConnectionFactory(httpConfiguration)).apply {


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestCustomJettyHttpConfiguration.kt b/javalin/src/test/java/io/javalin/TestCustomJettyHttpConfiguration.kt
index de29cd3c..9ceb50dc 100644
--- a/javalin/src/test/java/io/javalin/TestCustomJettyHttpConfiguration.kt
+++ b/javalin/src/test/java/io/javalin/TestCustomJettyHttpConfiguration.kt
@@ -79,4 +79,31 @@ class TestCustomJettyHttpConfiguration {
 
     }
 
+    @Test
+    fun `responseBufferSize - default is set to the jetty default for outputBufferSize`() = TestUtil.test { app, http ->
+        val firstHttpConnectionFactory = app.jettyServer().server().connectors.firstNotNullOfOrNull { (it as? ServerConnector)?.defaultConnectionFactory as? HttpConnectionFactory }
+        assertThat(app.unsafeConfig().http.responseBufferSize)
+            .isNotNull
+            .isEqualTo(firstHttpConnectionFactory?.httpConfiguration?.outputBufferSize)
+    }
+
+    @Test
+    fun `responseBufferSize - outputBufferSize set via jetty httpConfiguration is respected`() = TestUtil.test(Javalin.create { config ->
+        config.jetty.modifyHttpConfiguration { http -> http.outputBufferSize = 42_007 }
+    }) { app, http ->
+        assertThat(app.unsafeConfig().http.responseBufferSize)
+            .isNotNull
+            .isEqualTo(42_007)
+    }
+
+    @Test
+    fun `responseBufferSize - setting the javalin option has a higher priority than the jetty option`() = TestUtil.test(Javalin.create { config ->
+        config.http.responseBufferSize = 777_777
+        config.jetty.modifyHttpConfiguration { http -> http.outputBufferSize = 555_555 }
+    }) { app, http ->
+        assertThat(app.unsafeConfig().http.responseBufferSize)
+            .isNotNull
+            .isEqualTo(777_777)
+    }
+
 }