Repository: javalin/javalin
Pull request #2365 (2025-01-28 12:04:55)

Handle invalid http verbs in micrometer plugin
When requests come in with invalid methods (e.g. POSTS instead of POST) the exisitng  plugin fails to find the method and throws an exception (meaning no metrics are emitted).

Change to use the existng `findByName` method so that metrics are emitted. 

Created an issue https://github.com/javalin/javalin/issues/2366 to track this.


Patch:
diff --git a/javalin-micrometer/src/main/java/io/javalin/micrometer/MicrometerPlugin.kt b/javalin-micrometer/src/main/java/io/javalin/micrometer/MicrometerPlugin.kt
index 4d50954a..643b2a4a 100644
--- a/javalin-micrometer/src/main/java/io/javalin/micrometer/MicrometerPlugin.kt
+++ b/javalin-micrometer/src/main/java/io/javalin/micrometer/MicrometerPlugin.kt
@@ -65,7 +65,7 @@ class MicrometerPlugin(config: Consumer<MicrometerPluginConfig>) : Plugin<Microm
                     }
                     val pathInfo = request.pathInfo.removePrefix(config.router.contextPath).prefixIfNot("/")
                     response.setHeader(EXCEPTION_HEADER, null)
-                    val handlerType = HandlerType.valueOf(request.method)
+                    val handlerType = HandlerType.findByName(request.method)
                     val uri = internalRouter.findHttpHandlerEntries(handlerType, pathInfo)
                         .map { it.endpoint.path }
                         .map { if (it == "/" || it.isBlank()) "root" else it }
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/HttpStatus.kt b/javalin/src/main/java/io/javalin/http/HttpStatus.kt
index ddbc1218..e30da1de 100644
--- a/javalin/src/main/java/io/javalin/http/HttpStatus.kt
+++ b/javalin/src/main/java/io/javalin/http/HttpStatus.kt
@@ -149,9 +149,10 @@ enum class HttpStatus(val code: Int, val message: String) {
 
     companion object {
 
+        private val statusMap = HttpStatus.entries.associateBy { it.code }
+
         @JvmStatic
-        fun forStatus(status: Int): HttpStatus =
-            HttpStatus.entries.find { it.code == status } ?: UNKNOWN
+        fun forStatus(status: Int): HttpStatus = statusMap[status] ?: UNKNOWN
 
     }
 
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-micrometer/src/test/kotlin/io/javalin/micrometer/MicrometerPluginTest.kt b/javalin-micrometer/src/test/kotlin/io/javalin/micrometer/MicrometerPluginTest.kt
index d1153195..679682b7 100644
--- a/javalin-micrometer/src/test/kotlin/io/javalin/micrometer/MicrometerPluginTest.kt
+++ b/javalin-micrometer/src/test/kotlin/io/javalin/micrometer/MicrometerPluginTest.kt
@@ -227,6 +227,28 @@ class MicrometerPluginTest {
         assertThat(notFoundCountGeneric).isEqualTo(requestCount.toLong())
     }
 
+    @Test
+    fun invalidMethod() = JavalinTest.test(setupApp(tagNotFoundMappedPaths = true)) { app, http ->
+        val requestCount = (2..9).random()
+        app.get("/hello") { ctx ->
+            ctx.status(OK)
+        }
+        repeat(requestCount) {
+            http.request("/hello") { b -> b.method("POSTS", null) }
+        }
+
+        val notFoundCountGeneric = meterRegistry.get("jetty.server.requests")
+            .tag("uri", "NOT_FOUND")
+            .tag("method", "POSTS")
+            .tag("exception", "None")
+            .tag("status", "404")
+            .tag("outcome", "CLIENT_ERROR")
+            .timer()
+            .count()
+
+        assertThat(notFoundCountGeneric).isEqualTo(requestCount.toLong())
+    }
+
     private fun setupApp(
         tagRedirectPaths: Boolean = false,
         tagNotFoundMappedPaths: Boolean = false,
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)
+    }
+
 }