Repository: tipsy/javalin
Pull request #1543 (2022-05-13 10:03:54)

Allowing to get the allowed methods on a configured endpoint
The goal is to have relevant information if a person is working with a client-side app, for example a Vue or other apps where every request being done is preceded with an OPTIONS request to see if the request is going to be possible.

In some of our projects for example we had a preconditions check being done on an endpoint, but started to fail because there were no options returned.

Implemented a solution like this as a plugin for some of our projects to enable this, but think there would be more people helped with a thing like this.

tipsy: Thanks @MTijn! Functionality wise this looks fine. I added a couple of comments on the implementation, but nothing really important.

Patch:
diff --git a/javalin/src/main/java/io/javalin/core/JavalinConfig.java b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
index 76d40884..e24bbf07 100644
--- a/javalin/src/main/java/io/javalin/core/JavalinConfig.java
+++ b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
@@ -21,6 +21,7 @@ import io.javalin.core.util.CorsPlugin;
 import io.javalin.core.util.Header;
 import io.javalin.core.util.Headers;
 import io.javalin.core.util.HeadersPlugin;
+import io.javalin.core.util.HttpAllowedMethodsOnRoutesUtil;
 import io.javalin.core.util.LogUtil;
 import io.javalin.http.ContentType;
 import io.javalin.http.ContextResolver;
@@ -240,4 +241,8 @@ public class JavalinConfig {
         userResolver.accept(finalResolver);
         inner.appAttributes.put(CONTEXT_RESOLVER_KEY, finalResolver);
     }
+
+    public void enableHttpAllowedMethodsOnRoutes() {
+        registerPlugin(new HttpAllowedMethodsOnRoutesUtil());
+    }
 }
diff --git a/javalin/src/main/java/io/javalin/core/util/HttpAllowedMethodsOnRoutesUtil.kt b/javalin/src/main/java/io/javalin/core/util/HttpAllowedMethodsOnRoutesUtil.kt
new file mode 100644
index 00000000..f1ae14e1
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/core/util/HttpAllowedMethodsOnRoutesUtil.kt
@@ -0,0 +1,50 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2021 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+
+package io.javalin.core.util
+
+import io.javalin.Javalin
+import io.javalin.core.event.HandlerMetaInfo
+import io.javalin.core.plugin.Plugin
+import io.javalin.core.plugin.PluginLifecycleInit
+
+class HttpAllowedMethodsOnRoutesUtil() : Plugin, PluginLifecycleInit {
+    private val endpoints = mutableMapOf<String, MutableSet<HandlerMetaInfo>>()
+
+    override fun init(app: Javalin) {
+        app.events {
+            it.handlerAdded { handlerInfo ->
+                addOptionsToList(handlerInfo)
+            }
+        }
+    }
+
+    override fun apply(app: Javalin) {
+        app.events {
+            it.serverStarted {
+                createOptionsEndPoint(app)
+            }
+        }
+    }
+
+    private fun addOptionsToList(handlerMetaInfo: HandlerMetaInfo) {
+        val endpoint = endpoints.getOrPut(handlerMetaInfo.path) { mutableSetOf(handlerMetaInfo) }
+        endpoint.add(handlerMetaInfo)
+    }
+
+    private fun createOptionsEndPoint(app: Javalin) {
+        endpoints.forEach { endpoint ->
+            app.options(
+                endpoint.key
+            ) { context ->
+                context.header(
+                    Header.ACCESS_CONTROL_ALLOW_METHODS,
+                    endpoint.value.joinToString(",") { it.httpMethod.toString() }
+                )
+            }
+        }
+    }
+}


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestHttpAllowedMethodsOnRoutesUtil.kt b/javalin/src/test/java/io/javalin/TestHttpAllowedMethodsOnRoutesUtil.kt
new file mode 100644
index 00000000..a898e8d3
--- /dev/null
+++ b/javalin/src/test/java/io/javalin/TestHttpAllowedMethodsOnRoutesUtil.kt
@@ -0,0 +1,40 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2017 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+
+package io.javalin
+
+import io.javalin.core.util.Header
+import io.javalin.testing.TestUtil
+import kong.unirest.Unirest
+import org.assertj.core.api.Assertions
+import org.junit.jupiter.api.Test
+
+class TestHttpAllowedMethodsOnRoutesUtil {
+    @Test
+    fun `enableHttpOptionsForRoutes allows possible methods on routes`() {
+        val javalin = Javalin.create { it.enableHttpAllowedMethodsOnRoutes() }
+        javalin.get("/") { it.result("Hello") }
+        javalin.delete("/") { it.status(200) }
+
+        javalin.get("/users") { it.result("Users") }
+        javalin.post("/users") { it.status(201) }
+        javalin.patch("/users") { it.status(204) }
+
+        TestUtil.test(javalin) { app, http ->
+            val response = Unirest.options(http.origin)
+                .header(Header.ACCESS_CONTROL_REQUEST_HEADERS, "123")
+                .header(Header.ACCESS_CONTROL_REQUEST_METHOD, "TEST")
+                .asString()
+
+            Assertions.assertThat(response.headers[Header.ACCESS_CONTROL_ALLOW_METHODS]!![0])
+                .isEqualTo("GET,DELETE,OPTIONS")
+
+            val usersResponse = Unirest.options(http.origin + "/users").asString()
+            Assertions.assertThat(usersResponse.headers[Header.ACCESS_CONTROL_ALLOW_METHODS]!![0])
+                .isEqualTo("GET,POST,PATCH,OPTIONS")
+        }
+    }
+}