Repository: javalin/javalin
Pull request #2272 (2024-06-12 10:57:33)

Enable OSGi bundle packaging for Javalin v6.
This PR is similar to https://github.com/javalin/javalin/pull/1564 but aiming at Javalin v6.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/javalin/javalin/pull/2272?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 86.41%. Comparing base [(`ca81deb`)](https://app.codecov.io/gh/javalin/javalin/commit/ca81deb676bfcb7eee23f3b9a7c81bfff5c4e82c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=javalin) to head [(`3fca6b8`)](https://app.codecov.io/gh/javalin/javalin/commit/3fca6b8111782e281d7e2a0002c63f8c958d58f0?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    #2272   +/-   ##
=========================================
  Coverage     86.41%   86.41%           
  Complexity     1299     1299           
=========================================
  Files           148      148           
  Lines          4027     4027           
  Branches        506      506           
=========================================
  Hits           3480     3480           
  Misses          331      331           
  Partials        216      216           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/javalin/javalin/pull/2272?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).


Patch:
diff --git a/javalin-bundle/pom.xml b/javalin-bundle/pom.xml
index a7504622..415f289e 100644
--- a/javalin-bundle/pom.xml
+++ b/javalin-bundle/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-parent</artifactId>
         <groupId>io.javalin</groupId>
diff --git a/javalin-micrometer/pom.xml b/javalin-micrometer/pom.xml
index 969b8550..ad81c0e8 100644
--- a/javalin-micrometer/pom.xml
+++ b/javalin-micrometer/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-parent</artifactId>
         <groupId>io.javalin</groupId>
@@ -39,6 +40,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/javalin-osgi/README.md b/javalin-osgi/README.md
new file mode 100644
index 00000000..7826e621
--- /dev/null
+++ b/javalin-osgi/README.md
@@ -0,0 +1,2 @@
+Produced OSGi bundle of Javalin, currently exporting all the packages.
+This is done by re-packaging Javalin JAR into OSGi bundle and generating a suitable OSGi manifest file. 
diff --git a/javalin-osgi/pom.xml b/javalin-osgi/pom.xml
new file mode 100644
index 00000000..ef59b690
--- /dev/null
+++ b/javalin-osgi/pom.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>javalin-parent</artifactId>
+        <groupId>io.javalin</groupId>
+        <version>6.1.7-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>javalin-osgi</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.javalin</groupId>
+            <artifactId>javalin</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+
+    <packaging>bundle</packaging>
+    <properties>
+        <bundle-symbolic-name>${project.groupId}.${project.artifactId}</bundle-symbolic-name>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-everything</id>
+                        <phase>generate-resources</phase>
+                        <goals>
+                            <goal>unpack-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <includeScope>compile</includeScope>
+                            <includeGroupIds>${project.parent.groupId}</includeGroupIds>
+                            <outputDirectory>${project.build.outputDirectory}</outputDirectory>
+                            <excludes>**/MANIFEST.MF</excludes>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <version>5.1.6</version>
+                <extensions>true</extensions>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>bundle</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <excludeDependencies>true</excludeDependencies>
+                    <supportedProjectTypes>
+                        <supportedProjectType>jar</supportedProjectType>
+                        <supportedProjectType>bundle</supportedProjectType>
+                    </supportedProjectTypes>
+                    <instructions>
+                        <Bundle-SymbolicName>${bundle-symbolic-name}</Bundle-SymbolicName>
+                        <Bundle-Description>Bundle: ${project.name}</Bundle-Description>
+                        <Bundle-Name>Javalin OSGi bundle</Bundle-Name>
+                        <Import-Package>
+                            org.eclipse.jetty.util.component,\
+                            com.aayushatharva.brotli4j;resolution:=optional,\
+                            com.aayushatharva.brotli4j.encoder;resolution:=optional,\
+                            com.fasterxml.jackson.databind;resolution:=optional,\
+                            com.fasterxml.jackson.module.kotlin;resolution:=optional,\
+                            com.fasterxml.jackson.datatype.jsr310;resolution:=optional,\
+                            org.ktorm.jackson;resolution:=optional,\
+                            com.github.mustachejava;resolution:=optional,\
+                            com.google.gson;resolution:=optional,\
+                            com.mitchellbosecke.pebble;resolution:=optional,\
+                            com.mitchellbosecke.pebble.loader;resolution:=optional,\
+                            com.mitchellbosecke.pebble.template;resolution:=optional,\
+                            com.nixxcode.jvmbrotli.common;resolution:=optional,\
+                            com.nixxcode.jvmbrotli.enc;resolution:=optional,\
+                            freemarker.template;resolution:=optional,\
+                            gg.jte;resolution:=optional,\
+                            gg.jte.output;resolution:=optional,\
+                            gg.jte.resolve;resolution:=optional,\
+                            io.micrometer.core.instrument;resolution:=optional,\
+                            io.micrometer.core.instrument.binder.http;resolution:=optional,\
+                            io.micrometer.core.instrument.binder.jetty;resolution:=optional,\
+                            io.micrometer.core.instrument.composite;resolution:=optional,\
+                            io.swagger.util;resolution:=optional,\
+                            io.swagger.v3.parser;resolution:=optional,\
+                            io.swagger.v3.parser.core.models;resolution:=optional,\
+                            org.apache.velocity;resolution:=optional,\
+                            org.apache.velocity.app;resolution:=optional,\
+                            org.apache.velocity.context;resolution:=optional,\
+                            org.commonmark.node;resolution:=optional,\
+                            org.commonmark.parser;resolution:=optional,\
+                            org.commonmark.renderer.html;resolution:=optional,\
+                            org.jtwig;resolution:=optional,\
+                            org.jtwig.environment;resolution:=optional,\
+                            org.slf4j.spi;resolution:=optional,\
+                            org.thymeleaf;resolution:=optional,\
+                            org.thymeleaf.context;resolution:=optional,\
+                            org.thymeleaf.templatemode;resolution:=optional,\
+                            org.thymeleaf.templateresolver;resolution:=optional,
+                            kotlin.*, *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/javalin-rendering/pom.xml b/javalin-rendering/pom.xml
index 337bc4a5..26e68131 100644
--- a/javalin-rendering/pom.xml
+++ b/javalin-rendering/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-parent</artifactId>
         <groupId>io.javalin</groupId>
@@ -34,6 +35,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/javalin-ssl/pom.xml b/javalin-ssl/pom.xml
index f4ed788a..e7cbea00 100644
--- a/javalin-ssl/pom.xml
+++ b/javalin-ssl/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>io.javalin</groupId>
@@ -100,6 +101,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/javalin-utils/coverage/pom.xml b/javalin-utils/coverage/pom.xml
index 10005e69..c5afc716 100644
--- a/javalin-utils/coverage/pom.xml
+++ b/javalin-utils/coverage/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
     <parent>
diff --git a/javalin-utils/javalin-context-mock/pom.xml b/javalin-utils/javalin-context-mock/pom.xml
index e688f53d..a7b0c97f 100644
--- a/javalin-utils/javalin-context-mock/pom.xml
+++ b/javalin-utils/javalin-context-mock/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-utils</artifactId>
         <groupId>io.javalin</groupId>
@@ -43,6 +44,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/javalin-utils/javalin-context-mock/src/main/java/io/javalin/mock/ContextMock.kt b/javalin-utils/javalin-context-mock/src/main/java/io/javalin/mock/ContextMock.kt
index fca3a4c6..13795e3c 100644
--- a/javalin-utils/javalin-context-mock/src/main/java/io/javalin/mock/ContextMock.kt
+++ b/javalin-utils/javalin-context-mock/src/main/java/io/javalin/mock/ContextMock.kt
@@ -135,6 +135,7 @@ class ContextMock private constructor(
             defaultContentType = mockConfig.javalinConfig.http.defaultContentType,
             jsonMapper = mockConfig.javalinConfig.pvt.jsonMapper.value,
             requestLoggerEnabled = false,
+            strictContentTypes = false,
         )
 
 }
diff --git a/javalin-utils/pom.xml b/javalin-utils/pom.xml
index 4694239f..0e5aece7 100644
--- a/javalin-utils/pom.xml
+++ b/javalin-utils/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-parent</artifactId>
         <groupId>io.javalin</groupId>
diff --git a/javalin/pom.xml b/javalin/pom.xml
index e238566b..a3eaa191 100644
--- a/javalin/pom.xml
+++ b/javalin/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-parent</artifactId>
         <groupId>io.javalin</groupId>
@@ -71,6 +72,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
@@ -205,7 +212,7 @@
                             <goal>add-module-info</goal>
                         </goals>
                         <configuration>
-                        <overwriteExistingFiles>true</overwriteExistingFiles>
+                            <overwriteExistingFiles>true</overwriteExistingFiles>
                             <module>
                                 <moduleInfoFile>./module-info.java
                                 </moduleInfoFile>
diff --git a/javalin/src/main/java/io/javalin/config/HttpConfig.kt b/javalin/src/main/java/io/javalin/config/HttpConfig.kt
index 69db5ccc..09cc72c1 100644
--- a/javalin/src/main/java/io/javalin/config/HttpConfig.kt
+++ b/javalin/src/main/java/io/javalin/config/HttpConfig.kt
@@ -15,6 +15,7 @@ class HttpConfig(private val cfg: JavalinConfig) {
     //@formatter:off
     @JvmField var generateEtags = false
     @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 defaultContentType = ContentType.PLAIN
     @JvmField var asyncTimeout = 0L
diff --git a/javalin/src/main/java/io/javalin/http/Context.kt b/javalin/src/main/java/io/javalin/http/Context.kt
index 1c3f0292..f1576526 100644
--- a/javalin/src/main/java/io/javalin/http/Context.kt
+++ b/javalin/src/main/java/io/javalin/http/Context.kt
@@ -149,13 +149,19 @@ interface Context {
     }
 
     /** Maps a JSON body to a Java/Kotlin class using the registered [io.javalin.json.JsonMapper] */
-    fun <T> bodyAsClass(type: Type): T = jsonMapper().fromJsonString(body(), type)
+    fun <T> bodyAsClass(type: Type): T =  when {
+        isJson() || !strictContentTypes() -> jsonMapper().fromJsonString(body(), type)
+        else -> throw BadRequestResponse("Content-Type is not application/json")
+    }
 
     /** Maps a JSON body to a Java/Kotlin class using the registered [io.javalin.json.JsonMapper] */
     fun <T> bodyAsClass(clazz: Class<T>): T = bodyAsClass(type = clazz as Type)
 
     /** Maps a JSON body to a Java/Kotlin class using the registered [io.javalin.json.JsonMapper] */
-    fun <T> bodyStreamAsClass(type: Type): T = jsonMapper().fromJsonStream(req().inputStream, type)
+    fun <T> bodyStreamAsClass(type: Type): T = when {
+        isJson() || !strictContentTypes() -> jsonMapper().fromJsonStream(req().inputStream, type)
+        else -> throw BadRequestResponse("Content-Type is not application/json")
+    }
 
     /** Gets the underlying [InputStream] for the request body */
     fun bodyInputStream(): InputStream = req().inputStream
@@ -175,9 +181,12 @@ interface Context {
     /** Gets a map with all the form param keys and values. */
     fun formParamMap(): Map<String, List<String>> = when {
         isMultipartFormData() -> MultipartUtil.getFieldMap(req())
-        else -> splitKeyValueStringAndGroupByKey(body(), characterEncoding() ?: "UTF-8")
+        isFormUrlencoded() || !strictContentTypes() -> splitKeyValueStringAndGroupByKey(body(), characterEncoding() ?: "UTF-8")
+        else -> mapOf()
     }
 
+    fun strictContentTypes(): Boolean
+
     /**
      * Gets a path param by name (ex: pathParam("param").
      *
@@ -271,10 +280,16 @@ interface Context {
     fun basicAuthCredentials(): BasicAuthCredentials? = getBasicAuthCredentials(header(Header.AUTHORIZATION))
 
     /** Returns true if request is multipart. */
-    fun isMultipart(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.contains("multipart/") == true
+    fun isMultipart(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.startsWith("multipart/") == true
 
     /** Returns true if request is multipart/form-data. */
-    fun isMultipartFormData(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.contains("multipart/form-data") == true
+    fun isMultipartFormData(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.startsWith("multipart/form-data") == true
+
+    /** Returns true if request is application/x-www-form-urlencoded. */
+    fun isFormUrlencoded(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.startsWith("application/x-www-form-urlencoded") == true
+
+    /** Returns true if request is application/json. */
+    fun isJson(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.startsWith("application/json") == true
 
     /** Gets first [UploadedFile] for the specified name, or null. */
     fun uploadedFile(fileName: String): UploadedFile? = uploadedFiles(fileName).firstOrNull()
diff --git a/javalin/src/main/java/io/javalin/http/servlet/JavalinServletContext.kt b/javalin/src/main/java/io/javalin/http/servlet/JavalinServletContext.kt
index fdfe90e3..0e7e6631 100644
--- a/javalin/src/main/java/io/javalin/http/servlet/JavalinServletContext.kt
+++ b/javalin/src/main/java/io/javalin/http/servlet/JavalinServletContext.kt
@@ -47,7 +47,8 @@ data class JavalinServletContextConfig(
     val compressionStrategy: CompressionStrategy,
     val requestLoggerEnabled: Boolean,
     val defaultContentType: String,
-    val jsonMapper: JsonMapper
+    val jsonMapper: JsonMapper,
+    val strictContentTypes: Boolean
 ) {
     companion object {
         fun of(cfg: JavalinConfig): JavalinServletContextConfig =
@@ -58,6 +59,7 @@ data class JavalinServletContextConfig(
                 requestLoggerEnabled = cfg.pvt.requestLogger != null,
                 defaultContentType = cfg.http.defaultContentType,
                 jsonMapper = cfg.pvt.jsonMapper.value,
+                strictContentTypes = cfg.http.strictContentTypes,
             )
     }
 }
@@ -137,6 +139,10 @@ class JavalinServletContext(
     private val formParams by javalinLazy { super.formParamMap() }
     override fun formParamMap(): Map<String, List<String>> = formParams
 
+    override fun strictContentTypes(): Boolean {
+        return cfg.strictContentTypes
+    }
+
     override fun pathParamMap(): Map<String, String> = Collections.unmodifiableMap(pathParamMap)
     override fun pathParam(key: String): String = pathParamOrThrow(pathParamMap, key, matchedPath)
 
diff --git a/javalin/src/main/java/io/javalin/router/JavalinDefaultRoutingApi.kt b/javalin/src/main/java/io/javalin/router/JavalinDefaultRoutingApi.kt
index c6e59f84..6fd56a17 100644
--- a/javalin/src/main/java/io/javalin/router/JavalinDefaultRoutingApi.kt
+++ b/javalin/src/main/java/io/javalin/router/JavalinDefaultRoutingApi.kt
@@ -63,49 +63,49 @@ interface JavalinDefaultRoutingApi<API : RoutingApi> : RoutingApi {
 
     /**
      * Adds an exception mapper to the instance.
-     * See: [Exception mapping in docs](https://javalin.io/documentation.exception-mapping)
+     * See: [Exception mapping in docs](https://javalin.io/documentation#exception-mapping)
      */
     fun <E : Exception> exception(exceptionClass: Class<E>, exceptionHandler: ExceptionHandler<in E>): API
 
     /**
      * Adds an error mapper to the instance.
      * Useful for turning error-codes (404, 500) into standardized messages/pages
-     * See: [Error mapping in docs](https://javalin.io/documentation.error-mapping)
+     * See: [Error mapping in docs](https://javalin.io/documentation#error-mapping)
      */
     fun error(status: HttpStatus, handler: Handler): API = error(status.code, "*", handler)
 
     /**
      * Adds an error mapper to the instance.
      * Useful for turning error-codes (404, 500) into standardized messages/pages
-     * See: [Error mapping in docs](https://javalin.io/documentation.error-mapping)
+     * See: [Error mapping in docs](https://javalin.io/documentation#error-mapping)
      */
     fun error(status: Int, handler: Handler): API = error(status, "*", handler)
 
     /**
      * Adds an error mapper for the specified content-type to the instance.
      * Useful for turning error-codes (404, 500) into standardized messages/pages
-     * See: [Error mapping in docs](https://javalin.io/documentation.error-mapping)
+     * See: [Error mapping in docs](https://javalin.io/documentation#error-mapping)
      */
     fun error(status: HttpStatus, contentType: String, handler: Handler): API = error(status.code, contentType, handler)
 
     /**
      * Adds an error mapper for the specified content-type to the instance.
      * Useful for turning error-codes (404, 500) into standardized messages/pages
-     * See: [Error mapping in docs](https://javalin.io/documentation.error-mapping)
+     * See: [Error mapping in docs](https://javalin.io/documentation#error-mapping)
      */
     fun error(status: Int, contentType: String, handler: Handler): API
 
     /**
      * Adds a request handler for the specified handlerType and path to the instance.
      * This is the method that all the verb-methods (get/post/put/etc) call.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun addHttpHandler(httpMethod: HandlerType, path: String, handler: Handler): API = addHttpHandler(httpMethod, path, handler, *emptyArray())
 
     /**
      * Adds a request handler for the specified handlerType and path to the instance.
      * This is the method that all the verb-methods (get/post/put/etc) call.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun addHttpHandler(handlerType: HandlerType, path: String, handler: Handler, vararg roles: RouteRole): API =
         addEndpoint(
@@ -117,91 +117,91 @@ interface JavalinDefaultRoutingApi<API : RoutingApi> : RoutingApi {
     /**
      * Adds a request handler for the specified handlerType and path to the instance.
      * This is the method that all the verb-methods (get/post/put/etc) call.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun addEndpoint(endpoint: Endpoint): API
 
     /**
      * Adds a GET request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun get(path: String, handler: Handler): API = addHttpHandler(GET, path, handler)
 
     /**
      * Adds a POST request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun post(path: String, handler: Handler): API = addHttpHandler(POST, path, handler)
 
     /**
      * Adds a PUT request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun put(path: String, handler: Handler): API = addHttpHandler(PUT, path, handler)
 
     /**
      * Adds a PATCH request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun patch(path: String, handler: Handler): API = addHttpHandler(PATCH, path, handler)
 
     /**
      * Adds a DELETE request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun delete(path: String, handler: Handler): API = addHttpHandler(DELETE, path, handler)
 
     /**
      * Adds a HEAD request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun head(path: String, handler: Handler): API = addHttpHandler(HEAD, path, handler)
 
     /**
      * Adds a OPTIONS request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun options(path: String, handler: Handler): API = addHttpHandler(OPTIONS, path, handler)
 
     /**
      * Adds a GET request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun get(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(GET, path, handler, *roles)
 
     /**
      * Adds a POST request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun post(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(POST, path, handler, *roles)
 
     /**
      * Adds a PUT request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun put(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(PUT, path, handler, *roles)
 
     /**
      * Adds a PATCH request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun patch(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(PATCH, path, handler, *roles)
 
     /**
      * Adds a DELETE request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun delete(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(DELETE, path, handler, *roles)
 
     /**
      * Adds a HEAD request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun head(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(HEAD, path, handler, *roles)
 
     /**
      * Adds a OPTIONS request handler with the given roles for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#handlers)
      */
     fun options(path: String, handler: Handler, vararg roles: RouteRole): API = addHttpHandler(OPTIONS, path, handler, *roles)
 
@@ -222,73 +222,73 @@ interface JavalinDefaultRoutingApi<API : RoutingApi> : RoutingApi {
 
     /**
      * Adds a BEFORE request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun before(path: String, handler: Handler): API = addHttpHandler(BEFORE, path, handler)
 
     /**
      * Adds a BEFORE request handler for all routes in the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun before(handler: Handler): API = before("*", handler)
 
     /**
      * Adds a BEFORE_MATCHED request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun beforeMatched(path: String, handler: Handler): API = addHttpHandler(BEFORE_MATCHED, path, handler)
 
     /**
      * Adds a BEFORE_MATCHED request handler for all routes in the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun beforeMatched(handler: Handler): API = beforeMatched("*", handler)
 
     /**
      * Adds an AFTER request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun after(path: String, handler: Handler): API = addHttpHandler(AFTER, path, handler)
 
     /**
      * Adds an AFTER request handler for all routes in the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun after(handler: Handler): API = after("*", handler)
 
     /**
      * Adds an AFTER_MATCHED request handler for the specified path to the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun afterMatched(path: String, handler: Handler): API = addHttpHandler(AFTER_MATCHED, path, handler)
 
     /**
      * Adds an AFTER_MATCHED request handler for all routes in the instance.
-     * See: [Handlers in docs](https://javalin.io/documentation.before-handlers)
+     * See: [Handlers in docs](https://javalin.io/documentation#before-handlers)
      */
     fun afterMatched(handler: Handler): API = afterMatched("*", handler)
 
     /**
      * Adds a WebSocket exception mapper to the instance.
-     * See: [Exception mapping in docs](https://javalin.io/documentation.exception-mapping)
+     * See: [Exception mapping in docs](https://javalin.io/documentation#exception-mapping)
      */
     fun <E : Exception> wsException(exceptionClass: Class<E>, exceptionHandler: WsExceptionHandler<in E>): API
 
     /**
      * Adds a WebSocket handler of the specified type on the specified path.
-     * See: [WebSockets in docs](https://javalin.io/documentation.websockets)
+     * See: [WebSockets in docs](https://javalin.io/documentation#websockets)
      */
     fun addWsHandler(handlerType: WsHandlerType, path: String, wsConfig: Consumer<WsConfig>, vararg roles: RouteRole): API
 
     /**
      * Adds a WebSocket handler on the specified path.
-     * See: [WebSockets in docs](https://javalin.io/documentation.websockets)
+     * See: [WebSockets in docs](https://javalin.io/documentation#websockets)
      */
     fun ws(path: String, ws: Consumer<WsConfig>): API = ws(path, ws, *emptyArray())
 
     /**
      * Adds a WebSocket handler on the specified path with the specified roles.
-     * See: [WebSockets in docs](https://javalin.io/documentation.websockets)
+     * See: [WebSockets in docs](https://javalin.io/documentation#websockets)
      */
     fun ws(path: String, ws: Consumer<WsConfig>, vararg roles: RouteRole): API = addWsHandler(WEBSOCKET, path, ws, *roles)
 
diff --git a/pom.xml b/pom.xml
index acb0b208..773a722e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,4 +1,5 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <packaging>pom</packaging>
     <modules>
@@ -9,6 +10,7 @@
         <module>javalin-utils</module>
         <module>javalin-rendering</module>
         <module>javalin-ssl</module>
+        <module>javalin-osgi</module>
     </modules>
 
     <groupId>io.javalin</groupId>
@@ -78,7 +80,7 @@
         <shade.plugin.version>3.6.0</shade.plugin.version>
         <build-helper.plugin.version>3.6.0</build-helper.plugin.version>
         <enforcer.plugin.version>3.5.0</enforcer.plugin.version>
-        <clean.plugin.version>3.3.2</clean.plugin.version>
+        <clean.plugin.version>3.4.0</clean.plugin.version>
         <install.plugin.version>3.1.2</install.plugin.version>
         <resources.plugin.version>3.3.1</resources.plugin.version>
         <source.plugin.version>3.3.1</source.plugin.version>
@@ -105,7 +107,7 @@
         <assertj.version>3.26.0</assertj.version>
         <moshi.version>1.15.1</moshi.version>
         <java.websocket.version>1.5.6</java.websocket.version>
-        <junit.version>5.9.3</junit.version>
+        <junit.version>5.10.3</junit.version>
         <jmh.version>1.37</jmh.version>
         <mockito.version>5.12.0</mockito.version>
         <mockk.version>1.13.10</mockk.version>
@@ -452,9 +454,9 @@
                         Similar JUnit issue: https://github.com/junit-team/junit5/issues/1377
                         -->
                         <excludes>
-                            <exclude />
+                            <exclude/>
                         </excludes>
-          	            <useModulePath>false</useModulePath>
+                        <useModulePath>false</useModulePath>
                     </configuration>
                 </plugin>
                 <plugin>


Test patch:
diff --git a/javalin-testtools/pom.xml b/javalin-testtools/pom.xml
index 6c1255cd..501a552d 100644
--- a/javalin-testtools/pom.xml
+++ b/javalin-testtools/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
         <artifactId>javalin-parent</artifactId>
         <groupId>io.javalin</groupId>
@@ -41,6 +42,12 @@
             <version>${junit.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/javalin/src/test/java/io/javalin/TestBodyReading.kt b/javalin/src/test/java/io/javalin/TestBodyReading.kt
index d82768e3..6811f097 100644
--- a/javalin/src/test/java/io/javalin/TestBodyReading.kt
+++ b/javalin/src/test/java/io/javalin/TestBodyReading.kt
@@ -14,6 +14,7 @@ import java.net.URLEncoder
 import java.nio.charset.StandardCharsets
 
 class TestBodyReading {
+    private val strictContentTypeJavalin = Javalin.create { cfg -> cfg.http.strictContentTypes = true }
 
     @Test
     fun `reading body as bytes works`() = TestUtil.test { app, http ->
@@ -70,6 +71,20 @@ class TestBodyReading {
         assertThat(response.body).isEqualTo("null|valid")
     }
 
+    @Test
+    fun `reading form-params with strictContentTypes and without contentType returns nothing`() = TestUtil.test(strictContentTypeJavalin) { app, http ->
+        app.post("/") { it.result((it.formParam("fp") == null).toString()) }
+        val response = http.post("/").body("fp=param").asString()
+        assertThat(response.body).isEqualTo("true")
+    }
+
+    @Test
+    fun `reading form-params with strictContentTypes and without contentType works`() = TestUtil.test(strictContentTypeJavalin) { app, http ->
+        app.post("/") { it.result((it.formParam("fp")).toString()) }
+        val response = http.post("/").contentType("application/x-www-form-urlencoded").body("fp=param").asString()
+        assertThat(response.body).isEqualTo("param")
+    }
+
     @Test // not sure why this does so much...
     fun `query-params and form-params behave the same`() = TestUtil.test { app, http ->
         app.post("/") { ctx ->
diff --git a/javalin/src/test/java/io/javalin/TestConfiguration.kt b/javalin/src/test/java/io/javalin/TestConfiguration.kt
index a39533ac..e47eeb66 100644
--- a/javalin/src/test/java/io/javalin/TestConfiguration.kt
+++ b/javalin/src/test/java/io/javalin/TestConfiguration.kt
@@ -42,6 +42,7 @@ class TestConfiguration {
             it.bundledPlugins.enableRouteOverview("/test")
             it.bundledPlugins.enableSslRedirects()
             it.http.prefer405over404 = false
+            it.http.strictContentTypes = true
             it.requestLogger.http { ctx, timeInMs -> }
             it.requestLogger.ws { ws -> }
             it.showJavalinBanner = false
diff --git a/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi.kt b/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi.kt
index c94e7533..f850eecb 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi.kt
@@ -30,6 +30,7 @@ class TestJavalinInstanceAndConfigApi {
             app.http.maxRequestSize = 10_000L
             app.http.generateEtags = true
             app.http.prefer405over404 = true
+            app.http.strictContentTypes = true
             app.http.customCompression(CompressionStrategy())
             app.http.brotliAndGzipCompression(3)
             app.http.brotliOnlyCompression(3)
diff --git a/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi_Java.java b/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi_Java.java
index 52e5eab7..ef689a66 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi_Java.java
+++ b/javalin/src/test/java/io/javalin/TestJavalinInstanceAndConfigApi_Java.java
@@ -30,6 +30,7 @@ public class TestJavalinInstanceAndConfigApi_Java {
             app.http.maxRequestSize = 10_000L;
             app.http.generateEtags = true;
             app.http.prefer405over404 = true;
+            app.http.strictContentTypes = true;
             app.http.customCompression(new CompressionStrategy());
             app.http.brotliAndGzipCompression(3);
             app.http.brotliOnlyCompression(3);
diff --git a/javalin/src/test/java/io/javalin/TestJson.kt b/javalin/src/test/java/io/javalin/TestJson.kt
index f8cf1ed1..b481dcf0 100644
--- a/javalin/src/test/java/io/javalin/TestJson.kt
+++ b/javalin/src/test/java/io/javalin/TestJson.kt
@@ -29,6 +29,7 @@ import java.lang.reflect.Type
 import java.time.Instant
 
 internal class TestJson {
+    private val strictContentTypeJavalin = Javalin.create { cfg -> cfg.http.strictContentTypes = true }
 
     private val serializableObjectString = fasterJacksonMapper.toJsonString(SerializableObject())
 
@@ -87,6 +88,18 @@ internal class TestJson {
         assertThat(http.get("/").httpCode()).isEqualTo(INTERNAL_SERVER_ERROR)
     }
 
+    @Test
+    fun `default mapper maps json to object with strict json request`() = TestUtil.test(strictContentTypeJavalin) { app, http ->
+        app.post("/") { it.result(it.bodyAsClass<SerializableObject>().value1) }
+        assertThat(http.post("/").contentType("application/json").body(serializableObjectString).asString().body).isEqualTo("FirstValue")
+    }
+
+    @Test
+    fun `default mapper throws when mapping strict non-json request to class`() = TestUtil.test(strictContentTypeJavalin) { app, http ->
+        app.post("/") { it.result(it.bodyAsClass<SerializableObject>().value1) }
+        assertThat(http.post("/").body(serializableObjectString).asString().httpCode()).isEqualTo(BAD_REQUEST)
+    }
+
     @Test
     fun `mapping invalid json to class can be handle by validator`() = TestUtil.test { app, http ->
         app.get("/") { it.bodyValidator<NonSerializableObject>().get() }