Repository: tipsy/javalin
Pull request #1194 (2021-03-11 00:47:53)

Rename WsHandlerController & WsHandler
This PR follows up on https://github.com/tipsy/javalin/issues/1191#issuecomment-794137985

Besides the rename there are small documentation improvements and a pointless HashMap is removed from WsHandlerController. 


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8217c8be..c4859f9e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -51,7 +51,7 @@ io.javalin
 ├── websocket/                  // Everything related to WebSockets
 │   ├── WsContext.kt            // Wrapper class for WebSockets
 │   ├── JavalinWsServlet.kt     // Responsible for WebSocket upgrade, as well as switching between WebSocket and HTTP
-│   └── WsHandlerController.kt  // Responsible for Websocket request lifecycle (before, endpoint, after, logging)
+│   └── WsConnection.kt         // Keeps state of a WebSocket connection and handles incoming events
 └── Javalin.java                // Main public API, responsible for setting up and configuring the server
 ```
 
diff --git a/javalin/src/main/java/io/javalin/Javalin.java b/javalin/src/main/java/io/javalin/Javalin.java
index 60ab65c8..d32797c2 100644
--- a/javalin/src/main/java/io/javalin/Javalin.java
+++ b/javalin/src/main/java/io/javalin/Javalin.java
@@ -30,7 +30,7 @@ import io.javalin.http.sse.SseClient;
 import io.javalin.http.sse.SseHandler;
 import io.javalin.websocket.JavalinWsServlet;
 import io.javalin.websocket.WsExceptionHandler;
-import io.javalin.websocket.WsHandler;
+import io.javalin.websocket.WsHandlers;
 import io.javalin.websocket.WsHandlerType;
 import java.util.HashSet;
 import java.util.Set;
@@ -73,7 +73,7 @@ public class Javalin {
      * @see Javalin#create(Consumer)
      */
     public static Javalin create() {
-        return create(JavalinConfig.noopConfig);
+        return create(config -> {});
     }
 
     /**
@@ -102,7 +102,7 @@ public class Javalin {
 
     // Create a standalone (non-jetty dependent) Javalin
     public static Javalin createStandalone() {
-        return createStandalone(JavalinConfig.noopConfig);
+        return createStandalone(config -> {});
     }
 
     // Get JavalinServlet (for use in standalone mode)
@@ -547,17 +547,17 @@ public class Javalin {
      * Adds a specific WebSocket handler for the given path to the instance.
      * Requires an access manager to be set on the instance.
      */
-    private Javalin addWsHandler(@NotNull WsHandlerType handlerType, @NotNull String path, @NotNull Consumer<WsHandler> wsHandler, @NotNull Set<Role> roles) {
-        wsServlet.addHandler(handlerType, path, wsHandler, roles);
-        eventManager.fireWsHandlerAddedEvent(new WsHandlerMetaInfo(handlerType, Util.prefixContextPath(servlet.getConfig().contextPath, path), wsHandler, roles));
+    private Javalin addWsHandler(@NotNull WsHandlerType handlerType, @NotNull String path, @NotNull Consumer<WsHandlers> wsHandlers, @NotNull Set<Role> roles) {
+        wsServlet.addHandler(handlerType, path, wsHandlers, roles);
+        eventManager.fireWsHandlerAddedEvent(new WsHandlerMetaInfo(handlerType, Util.prefixContextPath(servlet.getConfig().contextPath, path), wsHandlers, roles));
         return this;
     }
 
     /**
      * Adds a specific WebSocket handler for the given path to the instance.
      */
-    private Javalin addWsHandler(@NotNull WsHandlerType handlerType, @NotNull String path, @NotNull Consumer<WsHandler> wsHandler) {
-        return addWsHandler(handlerType, path, wsHandler, new HashSet<>());
+    private Javalin addWsHandler(@NotNull WsHandlerType handlerType, @NotNull String path, @NotNull Consumer<WsHandlers> wsHandlers) {
+        return addWsHandler(handlerType, path, wsHandlers, new HashSet<>());
     }
 
     /**
@@ -565,7 +565,7 @@ public class Javalin {
      *
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
-    public Javalin ws(@NotNull String path, @NotNull Consumer<WsHandler> ws) {
+    public Javalin ws(@NotNull String path, @NotNull Consumer<WsHandlers> ws) {
         return ws(path, ws, new HashSet<>());
     }
 
@@ -576,36 +576,36 @@ public class Javalin {
      * @see AccessManager
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
-    public Javalin ws(@NotNull String path, @NotNull Consumer<WsHandler> ws, @NotNull Set<Role> permittedRoles) {
+    public Javalin ws(@NotNull String path, @NotNull Consumer<WsHandlers> ws, @NotNull Set<Role> permittedRoles) {
         return addWsHandler(WsHandlerType.WEBSOCKET, path, ws, permittedRoles);
     }
 
     /**
      * Adds a WebSocket before handler for the specified path to the instance.
      */
-    public Javalin wsBefore(@NotNull String path, @NotNull Consumer<WsHandler> wsHandler) {
-        return addWsHandler(WsHandlerType.WS_BEFORE, path, wsHandler);
+    public Javalin wsBefore(@NotNull String path, @NotNull Consumer<WsHandlers> wsHandlers) {
+        return addWsHandler(WsHandlerType.WS_BEFORE, path, wsHandlers);
     }
 
     /**
      * Adds a WebSocket before handler for all routes in the instance.
      */
-    public Javalin wsBefore(@NotNull Consumer<WsHandler> wsHandler) {
-        return wsBefore("*", wsHandler);
+    public Javalin wsBefore(@NotNull Consumer<WsHandlers> wsHandlers) {
+        return wsBefore("*", wsHandlers);
     }
 
     /**
      * Adds a WebSocket after handler for the specified path to the instance.
      */
-    public Javalin wsAfter(@NotNull String path, @NotNull Consumer<WsHandler> wsHandler) {
-        return addWsHandler(WsHandlerType.WS_AFTER, path, wsHandler);
+    public Javalin wsAfter(@NotNull String path, @NotNull Consumer<WsHandlers> wsHandlers) {
+        return addWsHandler(WsHandlerType.WS_AFTER, path, wsHandlers);
     }
 
     /**
      * Adds a WebSocket after handler for all routes in the instance.
      */
-    public Javalin wsAfter(@NotNull Consumer<WsHandler> wsHandler) {
-        return wsAfter("*", wsHandler);
+    public Javalin wsAfter(@NotNull Consumer<WsHandlers> wsHandlers) {
+        return wsAfter("*", wsHandlers);
     }
 
 }
diff --git a/javalin/src/main/java/io/javalin/apibuilder/ApiBuilder.java b/javalin/src/main/java/io/javalin/apibuilder/ApiBuilder.java
index c4eec4b2..13bd2c7a 100644
--- a/javalin/src/main/java/io/javalin/apibuilder/ApiBuilder.java
+++ b/javalin/src/main/java/io/javalin/apibuilder/ApiBuilder.java
@@ -11,7 +11,7 @@ import io.javalin.core.security.AccessManager;
 import io.javalin.core.security.Role;
 import io.javalin.http.Handler;
 import io.javalin.http.sse.SseClient;
-import io.javalin.websocket.WsHandler;
+import io.javalin.websocket.WsHandlers;
 import java.util.ArrayDeque;
 import java.util.Arrays;
 import java.util.Deque;
@@ -374,7 +374,7 @@ public class ApiBuilder {
      *
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
-    public static void ws(@NotNull String path, @NotNull Consumer<WsHandler> ws) {
+    public static void ws(@NotNull String path, @NotNull Consumer<WsHandlers> ws) {
         staticInstance().ws(prefixPath(path), ws);
     }
 
@@ -384,7 +384,7 @@ public class ApiBuilder {
      *
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
-    public static void ws(@NotNull String path, @NotNull Consumer<WsHandler> ws, @NotNull Set<Role> permittedRoles) {
+    public static void ws(@NotNull String path, @NotNull Consumer<WsHandlers> ws, @NotNull Set<Role> permittedRoles) {
         staticInstance().ws(prefixPath(path), ws, permittedRoles);
     }
 
@@ -394,7 +394,7 @@ public class ApiBuilder {
      *
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
-    public static void ws(@NotNull Consumer<WsHandler> ws) {
+    public static void ws(@NotNull Consumer<WsHandlers> ws) {
         staticInstance().ws(prefixPath(""), ws);
     }
 
@@ -404,7 +404,7 @@ public class ApiBuilder {
      *
      * @see <a href="https://javalin.io/documentation#websockets">WebSockets in docs</a>
      */
-    public static void ws(@NotNull Consumer<WsHandler> ws, @NotNull Set<Role> permittedRoles) {
+    public static void ws(@NotNull Consumer<WsHandlers> ws, @NotNull Set<Role> permittedRoles) {
         staticInstance().ws(prefixPath(""), ws, permittedRoles);
     }
 
@@ -412,32 +412,32 @@ public class ApiBuilder {
      * Adds a WebSocket before handler for the specified path to the {@link Javalin} instance.
      * The method can only be called inside a {@link Javalin#routes(EndpointGroup)}.
      */
-    public Javalin wsBefore(@NotNull String path, @NotNull Consumer<WsHandler> wsHandler) {
-        return staticInstance().wsBefore(prefixPath(path), wsHandler);
+    public Javalin wsBefore(@NotNull String path, @NotNull Consumer<WsHandlers> wsHandlers) {
+        return staticInstance().wsBefore(prefixPath(path), wsHandlers);
     }
 
     /**
      * Adds a WebSocket before handler for the current path to the {@link Javalin} instance.
      * The method can only be called inside a {@link Javalin#routes(EndpointGroup)}.
      */
-    public Javalin wsBefore(@NotNull Consumer<WsHandler> wsHandler) {
-        return staticInstance().wsBefore(prefixPath("/*"), wsHandler);
+    public Javalin wsBefore(@NotNull Consumer<WsHandlers> wsHandlers) {
+        return staticInstance().wsBefore(prefixPath("/*"), wsHandlers);
     }
 
     /**
      * Adds a WebSocket after handler for the specified path to the {@link Javalin} instance.
      * The method can only be called inside a {@link Javalin#routes(EndpointGroup)}.
      */
-    public Javalin wsAfter(@NotNull String path, @NotNull Consumer<WsHandler> wsHandler) {
-        return staticInstance().wsAfter(prefixPath(path), wsHandler);
+    public Javalin wsAfter(@NotNull String path, @NotNull Consumer<WsHandlers> wsHandlers) {
+        return staticInstance().wsAfter(prefixPath(path), wsHandlers);
     }
 
     /**
      * Adds a WebSocket after handler for the current path to the {@link Javalin} instance.
      * The method can only be called inside a {@link Javalin#routes(EndpointGroup)}.
      */
-    public Javalin wsAfter(@NotNull Consumer<WsHandler> wsHandler) {
-        return staticInstance().wsAfter(prefixPath("/*"), wsHandler);
+    public Javalin wsAfter(@NotNull Consumer<WsHandlers> wsHandlers) {
+        return staticInstance().wsAfter(prefixPath("/*"), wsHandlers);
     }
 
     // ********************************************************************************************
diff --git a/javalin/src/main/java/io/javalin/core/JavalinConfig.java b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
index a3c2b41e..bfd5c4e7 100644
--- a/javalin/src/main/java/io/javalin/core/JavalinConfig.java
+++ b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
@@ -26,7 +26,7 @@ import io.javalin.http.staticfiles.JettyResourceHandler;
 import io.javalin.http.staticfiles.Location;
 import io.javalin.http.staticfiles.ResourceHandler;
 import io.javalin.http.staticfiles.StaticFileConfig;
-import io.javalin.websocket.WsHandler;
+import io.javalin.websocket.WsHandlers;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -43,9 +43,6 @@ import org.jetbrains.annotations.Nullable;
 
 public class JavalinConfig {
     // @formatter:off
-    public static Consumer<JavalinConfig> noopConfig = JavalinConfig -> {}; // no change from default
-    //Left here for backwards compatibility only. Please use CompressionStrategy instead
-    @Deprecated public boolean dynamicGzip = true;
     public boolean autogenerateEtags = false;
     public boolean prefer405over404 = false;
     public boolean enforceSsl = false;
@@ -56,9 +53,7 @@ public class JavalinConfig {
     public boolean ignoreTrailingSlashes = true;
     @NotNull public String defaultContentType = "text/plain";
     @NotNull public String contextPath = "/";
-    //Left for backwards compatibility, please use maxRequestSize instead
-    @Deprecated @NotNull public Long requestCacheSize = 4096L;
-    public Long maxRequestSize;
+    public Long maxRequestSize = 1_000_000L; // server will not accept payloads larger than 1mb by default
     @NotNull public Long asyncRequestTimeout = 0L;
     @NotNull public Inner inner = new Inner();
 
@@ -73,7 +68,7 @@ public class JavalinConfig {
         @NotNull public SinglePageHandler singlePageHandler = new SinglePageHandler();
         @Nullable public SessionHandler sessionHandler = null;
         @Nullable public Consumer<WebSocketServletFactory> wsFactoryConfig = null;
-        @Nullable public WsHandler wsLogger = null;
+        @Nullable public WsHandlers wsLogger = null;
         @Nullable public Server server = null;
         @Nullable public Consumer<ServletContextHandler> servletContextHandlerConsumer = null;
         @NotNull public CompressionStrategy compressionStrategy = CompressionStrategy.GZIP;
@@ -174,8 +169,8 @@ public class JavalinConfig {
         return this;
     }
 
-    public JavalinConfig wsLogger(@NotNull Consumer<WsHandler> ws) {
-        WsHandler logger = new WsHandler();
+    public JavalinConfig wsLogger(@NotNull Consumer<WsHandlers> ws) {
+        WsHandlers logger = new WsHandlers();
         ws.accept(logger);
         inner.wsLogger = logger;
         return this;
@@ -204,11 +199,6 @@ public class JavalinConfig {
     public static void applyUserConfig(Javalin app, JavalinConfig config, Consumer<JavalinConfig> userConfig) {
         userConfig.accept(config); // apply user config to the default config
 
-        //Backwards compatibility. If deprecated dynamicGzip flag is set to false, disable compression.
-        if (!config.dynamicGzip) {
-            config.inner.compressionStrategy = CompressionStrategy.NONE;
-        }
-
         AtomicBoolean anyHandlerAdded = new AtomicBoolean(false);
         app.events(listener -> {
             listener.handlerAdded(x -> anyHandlerAdded.set(true));
diff --git a/javalin/src/main/java/io/javalin/core/util/LogUtil.kt b/javalin/src/main/java/io/javalin/core/util/LogUtil.kt
index e0cfe9bd..54c888b0 100644
--- a/javalin/src/main/java/io/javalin/core/util/LogUtil.kt
+++ b/javalin/src/main/java/io/javalin/core/util/LogUtil.kt
@@ -11,7 +11,7 @@ import io.javalin.http.Context
 import io.javalin.http.HandlerType
 import io.javalin.http.PathMatcher
 import io.javalin.websocket.WsContext
-import io.javalin.websocket.WsHandler
+import io.javalin.websocket.WsHandlers
 import java.util.*
 
 object LogUtil {
@@ -71,7 +71,7 @@ object LogUtil {
     fun executionTimeMs(ctx: Context) = (System.nanoTime() - ctx.attribute<Long>("javalin-request-log-start-time")!!) / 1000000f
 
     @JvmStatic
-    fun wsDevLogger(ws: WsHandler) {
+    fun wsDevLogger(ws: WsHandlers) {
         ws.onConnect { ctx -> ctx.logEvent("onConnect") }
         ws.onMessage { ctx -> ctx.logEvent("onMessage", "Message (next line):\n${ctx.message()}") }
         ws.onBinaryMessage { ctx -> ctx.logEvent("onBinaryMessage", "Offset: ${ctx.offset()}, Length: ${ctx.length()}\nMessage (next line):\n${ctx.data()}") }
diff --git a/javalin/src/main/java/io/javalin/http/Context.kt b/javalin/src/main/java/io/javalin/http/Context.kt
index 8058ade8..d8029161 100644
--- a/javalin/src/main/java/io/javalin/http/Context.kt
+++ b/javalin/src/main/java/io/javalin/http/Context.kt
@@ -12,6 +12,7 @@ import io.javalin.core.util.Header
 import io.javalin.core.validation.BodyValidator
 import io.javalin.core.validation.Validator
 import io.javalin.http.util.ContextUtil
+import io.javalin.http.util.ContextUtil.throwPayloadTooLargeIfPayloadTooLarge
 import io.javalin.http.util.CookieStore
 import io.javalin.http.util.MultipartUtil
 import io.javalin.http.util.SeekableWriter
@@ -46,7 +47,10 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     private val characterEncoding by lazy { ContextUtil.getRequestCharset(this) ?: "UTF-8" }
     private var resultStream: InputStream? = null
     private var resultFuture: CompletableFuture<*>? = null
-    private val body by lazy { req.inputStream.readBytes() }
+    private val body by lazy {
+        this.throwPayloadTooLargeIfPayloadTooLarge()
+        req.inputStream.readBytes()
+    }
 
     /**
      * Registers an extension to the Context, which can be used later in the request-lifecycle.
@@ -130,7 +134,7 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /**
      * Gets the request body as a [InputStream]
      */
-    fun bodyAsInputStream(): InputStream = body.inputStream()
+    fun bodyAsInputStream(): InputStream = req.inputStream
 
     /**
      * Creates a [Validator] for the body() value, with the prefix "Request body as $clazz"
diff --git a/javalin/src/main/java/io/javalin/http/JavalinServlet.kt b/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
index 2db76c62..cc730adc 100644
--- a/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
+++ b/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
@@ -14,6 +14,7 @@ import io.javalin.core.util.Header
 import io.javalin.core.util.LogUtil
 import io.javalin.core.util.Util
 import io.javalin.http.util.ContextUtil
+import io.javalin.http.util.ContextUtil.maxRequestSizeKey
 import io.javalin.http.util.MethodNotAllowedUtil
 import java.io.InputStream
 import java.util.concurrent.CompletionException
@@ -27,13 +28,13 @@ class JavalinServlet(val config: JavalinConfig) : HttpServlet() {
     val exceptionMapper = ExceptionMapper()
     val errorMapper = ErrorMapper()
 
-    override fun service(rawReq: HttpServletRequest, rawRes: HttpServletResponse) {
+    override fun service(req: HttpServletRequest, res: HttpServletResponse) {
         try {
-            val wrappedReq = RequestWrapper(rawReq, config)
-            val type = HandlerType.fromServletRequest(wrappedReq)
-            val rwc = ResponseWrapperContext(rawReq, config)
-            val requestUri = wrappedReq.requestURI.removePrefix(wrappedReq.contextPath)
-            val ctx = Context(wrappedReq, rawRes, config.inner.appAttributes)
+            val type = HandlerType.fromServletRequest(req)
+            val rwc = ResponseWrapperContext(req, config)
+            val requestUri = req.requestURI.removePrefix(req.contextPath)
+            val ctx = Context(req, res, config.inner.appAttributes)
+            ctx.attribute(maxRequestSizeKey, config.maxRequestSize)
 
             fun tryWithExceptionMapper(func: () -> Unit) = exceptionMapper.catchException(ctx, func)
 
@@ -49,7 +50,7 @@ class JavalinServlet(val config: JavalinConfig) : HttpServlet() {
                     return@tryWithExceptionMapper // return 200, there is a get handler
                 }
                 if (type == HandlerType.HEAD || type == HandlerType.GET) { // let Jetty check for static resources (will write response if found)
-                    if (config.inner.resourceHandler?.handle(wrappedReq, JavalinResponseWrapper(rawRes, rwc)) == true) return@tryWithExceptionMapper
+                    if (config.inner.resourceHandler?.handle(req, JavalinResponseWrapper(res, rwc)) == true) return@tryWithExceptionMapper
                     if (config.inner.singlePageHandler.handle(ctx)) return@tryWithExceptionMapper
                 }
                 if (type == HandlerType.OPTIONS && isCorsEnabled(config)) { // CORS is enabled, so we return 200 for OPTIONS
@@ -82,10 +83,10 @@ class JavalinServlet(val config: JavalinConfig) : HttpServlet() {
             if (ctx.resultFuture() == null) { // finish request synchronously
                 tryErrorHandlers()
                 tryAfterHandlers()
-                JavalinResponseWrapper(rawRes, rwc).write(ctx.resultStream())
+                JavalinResponseWrapper(res, rwc).write(ctx.resultStream())
                 config.inner.requestLogger?.handle(ctx, LogUtil.executionTimeMs(ctx))
             } else { // finish request asynchronously
-                val asyncContext = wrappedReq.startAsync().apply { timeout = config.asyncRequestTimeout }
+                val asyncContext = req.startAsync().apply { timeout = config.asyncRequestTimeout }
                 ctx.resultFuture()!!.exceptionally { throwable ->
                     if (throwable is CompletionException && throwable.cause is Exception) {
                         exceptionMapper.handle(throwable.cause as Exception, ctx)
@@ -109,7 +110,7 @@ class JavalinServlet(val config: JavalinConfig) : HttpServlet() {
             Unit // return void
         } catch (t: Throwable) {
             if (!Util.isClientAbortException(t)) {
-                rawRes.status = 500
+                res.status = 500
                 Javalin.log?.error("Exception occurred while servicing http-request", t)
             }
         }
diff --git a/javalin/src/main/java/io/javalin/http/RequestWrapper.kt b/javalin/src/main/java/io/javalin/http/RequestWrapper.kt
deleted file mode 100644
index 8eb05155..00000000
--- a/javalin/src/main/java/io/javalin/http/RequestWrapper.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Javalin - https://javalin.io
- * Copyright 2017 David Åse
- * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
- */
-
-package io.javalin.http
-
-import io.javalin.Javalin
-import io.javalin.core.JavalinConfig
-import org.eclipse.jetty.http.HttpStatus
-import javax.servlet.ServletInputStream
-import javax.servlet.http.HttpServletRequest
-import javax.servlet.http.HttpServletRequestWrapper
-
-class RequestWrapper(request: HttpServletRequest, private val config: JavalinConfig) : HttpServletRequestWrapper(request) {
-
-    private val bodySize = this.contentLengthLong
-
-    override fun getInputStream(): ServletInputStream {
-        config.maxRequestSize?.let {
-            if (bodySize > it) {
-                Javalin.log?.warn("Body greater than max size $it")
-                throw HttpResponseException(HttpStatus.PAYLOAD_TOO_LARGE_413, "Payload too large")
-            }
-        }
-        return super.getInputStream()
-    }
-}
diff --git a/javalin/src/main/java/io/javalin/http/UploadedFile.kt b/javalin/src/main/java/io/javalin/http/UploadedFile.kt
index 5be009be..61e4b235 100644
--- a/javalin/src/main/java/io/javalin/http/UploadedFile.kt
+++ b/javalin/src/main/java/io/javalin/http/UploadedFile.kt
@@ -18,4 +18,4 @@ import java.io.InputStream
  * @see Context.uploadedFile
  * @see <a href="https://javalin.io/documentation#faq">Uploads in FAQ</a>
  */
-data class UploadedFile(val content: InputStream, val contentType: String?, @Deprecated("Use UploadedFile.size", replaceWith = ReplaceWith("size")) val contentLength: Int, val filename: String, val extension: String, val size: Long)
+data class UploadedFile(val content: InputStream, val contentType: String?, val filename: String, val extension: String, val size: Long)
diff --git a/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt b/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt
index 479f1421..7195dfc3 100644
--- a/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt
+++ b/javalin/src/main/java/io/javalin/http/util/ContextUtil.kt
@@ -6,14 +6,18 @@
 
 package io.javalin.http.util
 
+import io.javalin.Javalin
 import io.javalin.core.security.BasicAuthCredentials
 import io.javalin.core.util.Header
 import io.javalin.http.Context
 import io.javalin.http.HandlerEntry
 import io.javalin.http.HandlerType
+import io.javalin.http.HttpResponseException
+import org.eclipse.jetty.http.HttpStatus
 import java.net.URL
 import java.net.URLDecoder
 import java.util.*
+import javax.servlet.ServletInputStream
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
@@ -89,4 +93,14 @@ object ContextUtil {
         this.matchedPath = ctx.matchedPath
     }
 
+    fun Context.throwPayloadTooLargeIfPayloadTooLarge() {
+        val maxRequestSize = this.attribute<Long>(maxRequestSizeKey)!!
+        if (this.req.contentLength > maxRequestSize) {
+            Javalin.log?.warn("Body greater than max size ($maxRequestSize bytes)")
+            throw HttpResponseException(HttpStatus.PAYLOAD_TOO_LARGE_413, "Payload too large")
+        }
+    }
+
+    const val maxRequestSizeKey = "javalin-max-request-size"
+
 }
diff --git a/javalin/src/main/java/io/javalin/http/util/MultipartUtil.kt b/javalin/src/main/java/io/javalin/http/util/MultipartUtil.kt
index 511fde83..61d23dfa 100644
--- a/javalin/src/main/java/io/javalin/http/util/MultipartUtil.kt
+++ b/javalin/src/main/java/io/javalin/http/util/MultipartUtil.kt
@@ -43,7 +43,6 @@ object MultipartUtil {
         return UploadedFile(
                 content = filePart.inputStream,
                 contentType = filePart.contentType,
-                contentLength = filePart.size.toInt(),
                 filename = filePart.submittedFileName,
                 extension = filePart.submittedFileName.replaceBeforeLast(".", ""),
                 size = filePart.size
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt b/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt
index 933aa6ff..71bafced 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt
@@ -6,162 +6,47 @@
 
 package io.javalin.plugin.rendering.vue
 
-import io.javalin.core.util.Header
 import io.javalin.http.Context
-import io.javalin.http.Handler
 import io.javalin.http.staticfiles.Location
 import io.javalin.http.util.ContextUtil.isLocalhost
-import io.javalin.plugin.json.JavalinJson
-import io.javalin.plugin.rendering.vue.FileInliner.inlineFiles
-import io.javalin.plugin.rendering.vue.JavalinVue.getAllDependencies
-import io.javalin.plugin.rendering.vue.JavalinVue.resourcesJarClass
 import java.nio.file.FileSystems
 import java.nio.file.Files
 import java.nio.file.Path
 import java.nio.file.Paths
-import java.util.regex.Matcher
 import java.util.stream.Collectors
+import io.javalin.plugin.rendering.vue.JavalinVue.resourcesJarClass as jarClass
 
 object JavalinVue {
-
-    internal var isDev: Boolean? = null
-    internal var vueDirPath: Path? = null
-
-    @JvmStatic
-    fun rootDirectory(path: String, location: Location) {
-        vueDirPath = if (location == Location.CLASSPATH) PathMaster.classpathPath(path) else Paths.get(path)
-    }
-
-    @JvmStatic
-    fun rootDirectory(path: Path) {
-        vueDirPath = path
+    // @formatter:off
+    internal var isDev: Boolean? = null // cached and easily accessible, is set on first request (can't be configured directly by end user)
+    @JvmField var isDevFunction: (Context) -> Boolean = { it.isLocalhost() } // used to set isDev, will be called once
+    @JvmField var optimizeDependencies = true // only include required components for the route component
+    @JvmField var resourcesJarClass = PathMaster::class // can be any class in the jar to look for resources in
+    @JvmField var stateFunction: (Context) -> Any = { mapOf<String, String>() } // global state that is injected into all VueComponents
+    @JvmField var cacheControl = "no-cache, no-store, must-revalidate"
+    @JvmField var rootDirectory: Path? = null // is set on first request (if not configured)
+    @JvmStatic fun rootDirectory(path: String, location: Location) {
+        rootDirectory = if (location == Location.CLASSPATH) PathMaster.classpathPath(path) else Paths.get(path)
     }
-
-    @JvmField
-    var optimizeDependencies = false
-
-    @JvmField
-    var stateFunction: (Context) -> Any = { mapOf<String, String>() }
-
-    @JvmField
-    var cacheControl = "no-cache, no-store, must-revalidate"
-
-    @JvmField
-    var isDevFunction: (Context) -> Boolean = { it.isLocalhost() }
-
-    @JvmField
-    var resourcesJarClass = PathMaster::class
-
-    internal fun walkPaths(): Set<Path> = Files.walk(vueDirPath, 10).collect(Collectors.toSet())
-
+    internal fun walkPaths(): Set<Path> = Files.walk(rootDirectory, 20).collect(Collectors.toSet())
     internal val cachedPaths by lazy { walkPaths() }
     internal val cachedDependencyResolver by lazy { VueDependencyResolver(cachedPaths) }
-
-    internal fun createLayout(paths: Set<Path>, componentDependencies: String): String {
-        return paths.find { it.endsWith("vue/layout.html") }!!.readText()
-                .inlineFiles(paths)
-                .replace("@componentRegistration", "@componentRegistration@serverState") // add state anchor for later
-                .replace("@componentRegistration", componentDependencies)
-                .replaceWebjarsWithCdn()
-    }
-
-    internal fun getAllDependencies(paths: Set<Path>) = paths.filter { it.isVueFile() }
-            .joinToString("") { "\n<!-- ${it.fileName} -->\n" + it.readText() }
-
-    // replace("\\", "\\\\") because we need the backslash to remain after evaluating the string
-    internal fun getState(ctx: Context, state: Any?) = "\n<script>\n" + """
-        |    Vue.prototype.${"$"}javalin = {
-        |        pathParams: `${htmlEscape(JavalinJson.toJson(ctx.pathParamMap()))?.replace("\\", "\\\\")}`,
-        |        queryParams: `${htmlEscape(JavalinJson.toJson(ctx.queryParamMap()))?.replace("\\", "\\\\")}`,
-        |        state: ${JavalinJson.toJson(state ?: stateFunction(ctx))}
-        |    }""".trimMargin() + "\n</script>\n" + decodeParams()
-
-    private fun decodeParams() = "\n<script>\n" + """
-        |    function ____decode(string) { // used for decoding HTML encoded params
-        |        let textArea = document.createElement("textarea");
-        |        textArea.innerHTML = string;
-        |        return textArea.value;
-        |    }
-        |    ["queryParams", "pathParams"].forEach((key) => {
-        |        Vue.prototype.${"$"}javalin[key] = JSON.parse(____decode(Vue.prototype.${"$"}javalin[key]));
-        |    });""".trimMargin() + "\n</script>\n"
-
-    private fun String.replaceWebjarsWithCdn() =
-            this.replace("@cdnWebjar/", if (isDev == true) "/webjars/" else "https://cdn.jsdelivr.net/webjars/org.webjars.npm/")
-
-}
-
-
-class VueComponent @JvmOverloads constructor(private val component: String, private val state: Any? = null) : Handler {
-    override fun handle(ctx: Context) {
-        JavalinVue.isDev = JavalinVue.isDev ?: JavalinVue.isDevFunction(ctx)
-        JavalinVue.vueDirPath = JavalinVue.vueDirPath ?: PathMaster.defaultLocation(JavalinVue.isDev)
-        val routeComponent = if (component.startsWith("<")) component else "<$component></$component>"
-        val paths = if (JavalinVue.isDev == true) JavalinVue.walkPaths() else JavalinVue.cachedPaths
-        val componentId = routeComponent.removePrefix("<").takeWhile { it !in setOf('>', ' ') }
-        val dependencyResolver by lazy { if (JavalinVue.isDev == true) VueDependencyResolver(paths) else JavalinVue.cachedDependencyResolver }
-        val view = JavalinVue.createLayout(paths, if (JavalinVue.optimizeDependencies) dependencyResolver.resolve(componentId) else getAllDependencies(paths))
-        if (!view.contains(componentId)) {
-            ctx.result("Route component not found: $routeComponent")
-            return
-        }
-        ctx.header(Header.CACHE_CONTROL, JavalinVue.cacheControl)
-        ctx.html(view.replace("@serverState", JavalinVue.getState(ctx, state)).replace("@routeComponent", routeComponent)) // insert current route component
-    }
+    // @formatter:on
 }
 
+/**
+ * By default, [jarClass] is PathMaster::class, which means this code will only
+ * work if the resources are in the same jar as Javalin (i.e. in a fat-jar/uber-jar).
+ * You can change resourcesJarClass to whatever class suits your needs.
+ */
 object PathMaster {
-    /**
-     * By default, resourcesJarClass is PathMaster::class, which means this code will only
-     * work if the resources are in the same jar as Javalin (i.e. in a fat-jar/uber-jar).
-     * You can change resourcesJarClass to whatever class suits your needs.
-     *
-     * Creating a filesystem is required since we want to "walk" the jar (see [JavalinVue.walkPaths])
-     * to find all the .vue files.
-     */
-    private val fileSystem by lazy { FileSystems.newFileSystem(resourcesJarClass.java.getResource("").toURI(), emptyMap<String, Any>()) }
+    /** We create a filesystem to "walk" the jar ([JavalinVue.walkPaths]) to find all the .vue files. */
+    private val fileSystem by lazy { FileSystems.newFileSystem(jarClass.java.getResource("").toURI(), emptyMap<String, Any>()) }
 
     fun classpathPath(path: String): Path = when {
-        resourcesJarClass.java.getResource(path).toURI().scheme == "jar" -> fileSystem.getPath(path) // we're inside a jar
-        else -> Paths.get(resourcesJarClass.java.getResource(path).toURI()) // we're not in jar (probably running from IDE)
+        jarClass.java.getResource(path).toURI().scheme == "jar" -> fileSystem.getPath(path) // we're inside a jar
+        else -> Paths.get(jarClass.java.getResource(path).toURI()) // we're not in jar (probably running from IDE)
     }
 
     fun defaultLocation(isDev: Boolean?) = if (isDev == true) Paths.get("src/main/resources/vue") else classpathPath("/vue")
 }
-
-object FileInliner {
-    private val newlineRegex = Regex("\\r?\\n")
-    private val unconditionalRegex = Regex("""@inlineFile\(".*"\)""")
-    private val devRegex = Regex("""@inlineFileDev\(".*"\)""")
-    private val notDevRegex = Regex("""@inlineFileNotDev\(".*"\)""")
-
-    fun String.inlineFiles(paths: Set<Path>): String {
-        val pathMap = paths.filterNot { it.isVueFile() } // vue files are inlined in @componentRegistration later
-                .associateBy { """"/vue/${it.toString().replace("\\", "/").substringAfter("/vue/")}"""" } // normalize keys
-        return this.split(newlineRegex).joinToString("\n") { line ->
-            if (!line.contains("@inlineFile")) return@joinToString line // nothing to inline
-            val matchingKey = pathMap.keys.find { line.contains(it) } ?: throw IllegalStateException("Invalid path found: $line")
-            val matchingFileContent by lazy { Matcher.quoteReplacement(pathMap[matchingKey]!!.readText()) }
-            when {
-                devRegex.containsMatchIn(line) -> if (JavalinVue.isDev == true) line.replace(devRegex, matchingFileContent) else ""
-                notDevRegex.containsMatchIn(line) -> if (JavalinVue.isDev == false) line.replace(notDevRegex, matchingFileContent) else ""
-                else -> line.replace(unconditionalRegex, matchingFileContent)
-            }
-        }
-    }
-}
-
-fun Path.readText() = String(Files.readAllBytes(this))
-fun Path.isVueFile() = this.toString().endsWith(".vue")
-
-fun htmlEscape(string: String?) = string?.toCharArray()?.map {
-    when (it) {
-        '<' -> "&lt;"
-        '>' -> "&gt;"
-        '&' -> "&amp;"
-        '"' -> "&quot;"
-        '\'' -> "&#x27;"
-        '/' -> "&#x2F;"
-        else -> it
-    }
-}?.joinToString("")
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt
new file mode 100644
index 00000000..6ffa7086
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueComponent.kt
@@ -0,0 +1,100 @@
+package io.javalin.plugin.rendering.vue
+
+import io.javalin.core.util.Header
+import io.javalin.http.Context
+import io.javalin.http.Handler
+import io.javalin.http.InternalServerErrorResponse
+import io.javalin.plugin.json.JavalinJson
+import io.javalin.plugin.rendering.vue.FileInliner.inlineFiles
+import io.javalin.plugin.rendering.vue.JavalinVue.cacheControl
+import io.javalin.plugin.rendering.vue.JavalinVue.cachedDependencyResolver
+import io.javalin.plugin.rendering.vue.JavalinVue.cachedPaths
+import io.javalin.plugin.rendering.vue.JavalinVue.isDev
+import io.javalin.plugin.rendering.vue.JavalinVue.isDevFunction
+import io.javalin.plugin.rendering.vue.JavalinVue.optimizeDependencies
+import io.javalin.plugin.rendering.vue.JavalinVue.rootDirectory
+import io.javalin.plugin.rendering.vue.JavalinVue.stateFunction
+import io.javalin.plugin.rendering.vue.JavalinVue.walkPaths
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+
+class VueComponent @JvmOverloads constructor(val component: String, val state: Any? = null) : Handler {
+    override fun handle(ctx: Context) {
+        isDev = isDev ?: isDevFunction(ctx)
+        rootDirectory = rootDirectory ?: PathMaster.defaultLocation(isDev)
+        val routeComponent = if (component.startsWith("<")) component else "<$component></$component>"
+        val allFiles = if (isDev == true) walkPaths() else cachedPaths
+        val resolver by lazy { if (isDev == true) VueDependencyResolver(allFiles) else cachedDependencyResolver }
+        val componentId = routeComponent.removePrefix("<").takeWhile { it !in setOf('>', ' ') }
+        val dependencies = if (optimizeDependencies) resolver.resolve(componentId) else allFiles.joinVueFiles()
+        if (componentId !in dependencies) throw InternalServerErrorResponse("Route component not found: $routeComponent")
+        ctx.html(allFiles.find { it.endsWith("vue/layout.html") }!!.readText() // we start with the layout file
+                .inlineFiles(allFiles.filterNot { it.isVueFile() }) // we then inline css/js files
+                .replace("@componentRegistration", "@componentRegistration@serverState") // add @serverState anchor for later
+                .replace("@componentRegistration", dependencies) // add all dependencies
+                .replace("@serverState", getState(ctx, state)) // add the way too complex state
+                .replace("@routeComponent", routeComponent) // finally, add the route component itself
+                .replace("@cdnWebjar/", if (isDev == true) "/webjars/" else "https://cdn.jsdelivr.net/webjars/org.webjars.npm/")
+        ).header(Header.CACHE_CONTROL, cacheControl)
+    }
+}
+
+object FileInliner {
+    private val newlineRegex = Regex("\\r?\\n")
+    private val unconditionalRegex = Regex("""@inlineFile\(".*"\)""")
+    private val devRegex = Regex("""@inlineFileDev\(".*"\)""")
+    private val notDevRegex = Regex("""@inlineFileNotDev\(".*"\)""")
+
+    fun String.inlineFiles(nonVueFiles: List<Path>): String {
+        val pathMap = nonVueFiles.associateBy { """"/vue/${it.toString().replace("\\", "/").substringAfter("/vue/")}"""" } // normalize keys
+        return this.split(newlineRegex).joinToString("\n") { line ->
+            if (!line.contains("@inlineFile")) return@joinToString line // nothing to inline
+            val matchingKey = pathMap.keys.find { line.contains(it) } ?: throw IllegalStateException("Invalid path found: $line")
+            val matchingFileContent by lazy { Matcher.quoteReplacement(pathMap[matchingKey]!!.readText()) }
+            when {
+                devRegex.containsMatchIn(line) -> if (isDev == true) line.replace(devRegex, matchingFileContent) else ""
+                notDevRegex.containsMatchIn(line) -> if (isDev == false) line.replace(notDevRegex, matchingFileContent) else ""
+                else -> line.replace(unconditionalRegex, matchingFileContent)
+            }
+        }
+    }
+}
+
+private fun Set<Path>.joinVueFiles() = this.filter { it.isVueFile() }.joinToString("") { "\n<!-- ${it.fileName} -->\n" + it.readText() }
+
+internal fun getState(ctx: Context, state: Any?): String {
+
+    fun htmlEscape(string: String?) = string?.toCharArray()?.map {
+        when (it) {
+            '<' -> "&lt;"
+            '>' -> "&gt;"
+            '&' -> "&amp;"
+            '"' -> "&quot;"
+            '\'' -> "&#x27;"
+            '/' -> "&#x2F;"
+            else -> it
+        }
+    }?.joinToString("")
+
+    fun decodeParams() = "\n<script>\n" + """
+        |    function ____decode(string) { // used for decoding HTML encoded params
+        |        let textArea = document.createElement("textarea");
+        |        textArea.innerHTML = string;
+        |        return textArea.value;
+        |    }
+        |    ["queryParams", "pathParams"].forEach((key) => {
+        |        Vue.prototype.${"$"}javalin[key] = JSON.parse(____decode(Vue.prototype.${"$"}javalin[key]));
+        |    });""".trimMargin() + "\n</script>\n"
+
+    // replace("\\", "\\\\") because we need the backslash to remain after evaluating the string
+    return "\n<script>\n" + """
+        |    Vue.prototype.${"$"}javalin = {
+        |        pathParams: `${htmlEscape(JavalinJson.toJson(ctx.pathParamMap()))?.replace("\\", "\\\\")}`,
+        |        queryParams: `${htmlEscape(JavalinJson.toJson(ctx.queryParamMap()))?.replace("\\", "\\\\")}`,
+        |        state: ${JavalinJson.toJson(state ?: stateFunction(ctx))}
+        |    }""".trimMargin() + "\n</script>\n" + decodeParams()
+}
+
+internal fun Path.readText() = String(Files.readAllBytes(this))
+internal fun Path.isVueFile() = this.toString().endsWith(".vue")
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java
index ebe66db9..8b805aa3 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/vue/VueDependencyResolver.java
@@ -36,8 +36,8 @@ public class VueDependencyResolver {
     public VueDependencyResolver(final Set<Path> paths) {
         componentIdToOwnContent = new HashMap<>();
         componentIdToDependencyContent = new HashMap<>();
-        paths.stream().filter(JavalinVueKt::isVueFile).forEach(path -> {
-            String fileContent = JavalinVueKt.readText(path);
+        paths.stream().filter(VueComponentKt::isVueFile).forEach(path -> {
+            String fileContent = VueComponentKt.readText(path);
             Matcher matcher = componentRegex.matcher(fileContent); // check for a vue component
             while (matcher.find()) {
                 componentIdToOwnContent.put(matcher.group(1), fileContent); // cache the file content, bound to the component name
@@ -63,7 +63,7 @@ public class VueDependencyResolver {
 
         StringBuilder builder = new StringBuilder();
         dependencies.forEach(dependency -> {
-            builder.append("<!-- ").append(dependency).append("-->\n");
+            builder.append("<!-- ").append(dependency).append(" -->\n");
             builder.append(componentIdToOwnContent.get(dependency));
             builder.append("\n");
         });
@@ -82,8 +82,8 @@ public class VueDependencyResolver {
     private Set<String> resolveTransitiveDependencies(final String componentId) {
         Set<String> requiredComponents = new HashSet<>();
         requiredComponents.add(componentId);// add it to the dependency list
-        Set<String> directDependencies = resolveDirectDependencies(componentId); //get its dependencies
-        requiredComponents.addAll(directDependencies); //add all its dependencies  to the required components list
+        Set<String> directDependencies = resolveDirectDependencies(componentId); // get its dependencies
+        requiredComponents.addAll(directDependencies); // add all its dependencies  to the required components list
         directDependencies.forEach(dependency -> {
             // resolve each dependency
             requiredComponents.addAll(resolveTransitiveDependencies(dependency));
@@ -100,11 +100,11 @@ public class VueDependencyResolver {
     private Set<String> resolveDirectDependencies(final String componentId) {
         Set<String> dependencies = new HashSet<>();
         String componentContent = componentIdToOwnContent.get(componentId);
-        Matcher matcher = tagRegex.matcher(componentContent); //match for HTML tags
+        Matcher matcher = tagRegex.matcher(componentContent); // match for HTML tags
         while (matcher.find()) {
             String match = matcher.group(1);
             if (!match.equals(componentId) && componentIdToOwnContent.containsKey(match)) { // if it isn't the component itself, and its in the component map
-                dependencies.add(match); //add it to the list of dependencies
+                dependencies.add(match); // add it to the list of dependencies
             }
         }
         return dependencies;
diff --git a/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt b/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
index 4b6632de..9ac4f83f 100644
--- a/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
+++ b/javalin/src/main/java/io/javalin/websocket/JavalinWsServlet.kt
@@ -42,7 +42,7 @@ class JavalinWsServlet(val config: JavalinConfig, private val httpServlet: Javal
             val preUpgradeContext = req.httpServletRequest.getAttribute(upgradeContextKey) as Context
             req.httpServletRequest.setAttribute(upgradeContextKey, ContextUtil.changeBaseRequest(preUpgradeContext, req.httpServletRequest))
             req.httpServletRequest.setAttribute(upgradeSessionAttrsKey, req.session?.attributeNames?.asSequence()?.associateWith { req.session.getAttribute(it) })
-            return@WebSocketCreator WsHandlerController(wsPathMatcher, wsExceptionMapper, config.inner.wsLogger)
+            return@WebSocketCreator WsConnection(wsPathMatcher, wsExceptionMapper, config.inner.wsLogger)
         }
     }
 
@@ -75,8 +75,9 @@ class JavalinWsServlet(val config: JavalinConfig, private val httpServlet: Javal
         }
     }
 
-    fun addHandler(handlerType: WsHandlerType, path: String, ws: Consumer<WsHandler>, permittedRoles: Set<Role>) {
-        wsPathMatcher.add(WsEntry(handlerType, path, config.ignoreTrailingSlashes, WsHandler().apply { ws.accept(this) }, permittedRoles))
+    fun addHandler(handlerType: WsHandlerType, path: String, ws: Consumer<WsHandlers>, permittedRoles: Set<Role>) {
+        wsPathMatcher.add(WsEntry(handlerType, path, config.ignoreTrailingSlashes, WsHandlers()
+            .apply { ws.accept(this) }, permittedRoles))
     }
 }
 
diff --git a/javalin/src/main/java/io/javalin/websocket/WsHandlerController.kt b/javalin/src/main/java/io/javalin/websocket/WsConnection.kt
similarity index 55%
rename from javalin/src/main/java/io/javalin/websocket/WsHandlerController.kt
rename to javalin/src/main/java/io/javalin/websocket/WsConnection.kt
index 974f6d59..3acf6cc7 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsHandlerController.kt
+++ b/javalin/src/main/java/io/javalin/websocket/WsConnection.kt
@@ -14,56 +14,53 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage
 import org.eclipse.jetty.websocket.api.annotations.WebSocket
 import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest
 import java.util.*
-import java.util.concurrent.ConcurrentHashMap
 
 /**
- * A special WebSocket handler which delegates the handling of the specific WebSocket event to the
- * matching custom handlers.
+ * Is instantiated for every WebSocket connection. It keeps the session and sessionId and handles incoming events by
+ * delegating to the registered before, endpoint, after and logger handlers.
  */
 @WebSocket
-class WsHandlerController(val matcher: WsPathMatcher, val exceptionMapper: WsExceptionMapper, val wsLogger: WsHandler?) {
+class WsConnection(val matcher: WsPathMatcher, val exceptionMapper: WsExceptionMapper, val wsLogger: WsHandlers?) {
 
-    private val sessionIds = ConcurrentHashMap<Session, String>()
+    private val sessionId: String = UUID.randomUUID().toString()
 
     @OnWebSocketConnect
     fun onConnect(session: Session) {
-        sessionIds[session] = UUID.randomUUID().toString() // associate a unique ID with this new session
-        val ctx = WsConnectContext(sessionIds[session]!!, session)
-        tryBeforeAndEndpointHandlers(ctx) { it.handler.wsConnectHandler?.handleConnect(ctx) }
-        tryAfterHandlers(ctx) { it.handler.wsConnectHandler?.handleConnect(ctx) }
+        val ctx = WsConnectContext(sessionId, session)
+        tryBeforeAndEndpointHandlers(ctx) { it.handlers.wsConnectHandler?.handleConnect(ctx) }
+        tryAfterHandlers(ctx) { it.handlers.wsConnectHandler?.handleConnect(ctx) }
         wsLogger?.wsConnectHandler?.handleConnect(ctx)
     }
 
     @OnWebSocketMessage
     fun onMessage(session: Session, message: String) {
-        val ctx = WsMessageContext(sessionIds[session]!!, session, message)
-        tryBeforeAndEndpointHandlers(ctx) { it.handler.wsMessageHandler?.handleMessage(ctx) }
-        tryAfterHandlers(ctx) { it.handler.wsMessageHandler?.handleMessage(ctx) }
+        val ctx = WsMessageContext(sessionId, session, message)
+        tryBeforeAndEndpointHandlers(ctx) { it.handlers.wsMessageHandler?.handleMessage(ctx) }
+        tryAfterHandlers(ctx) { it.handlers.wsMessageHandler?.handleMessage(ctx) }
         wsLogger?.wsMessageHandler?.handleMessage(ctx)
     }
 
     @OnWebSocketMessage
     fun onMessage(session: Session, buffer: ByteArray, offset: Int, length: Int) {
-        val ctx = WsBinaryMessageContext(sessionIds[session]!!, session, buffer, offset, length)
-        tryBeforeAndEndpointHandlers(ctx) { it.handler.wsBinaryMessageHandler?.handleBinaryMessage(ctx) }
-        tryAfterHandlers(ctx) { it.handler.wsBinaryMessageHandler?.handleBinaryMessage(ctx) }
+        val ctx = WsBinaryMessageContext(sessionId, session, buffer, offset, length)
+        tryBeforeAndEndpointHandlers(ctx) { it.handlers.wsBinaryMessageHandler?.handleBinaryMessage(ctx) }
+        tryAfterHandlers(ctx) { it.handlers.wsBinaryMessageHandler?.handleBinaryMessage(ctx) }
         wsLogger?.wsBinaryMessageHandler?.handleBinaryMessage(ctx)
     }
 
     @OnWebSocketClose
     fun onClose(session: Session, statusCode: Int, reason: String?) {
-        val ctx = WsCloseContext(sessionIds[session]!!, session, statusCode, reason)
-        tryBeforeAndEndpointHandlers(ctx) { it.handler.wsCloseHandler?.handleClose(ctx) }
-        tryAfterHandlers(ctx) { it.handler.wsCloseHandler?.handleClose(ctx) }
+        val ctx = WsCloseContext(sessionId, session, statusCode, reason)
+        tryBeforeAndEndpointHandlers(ctx) { it.handlers.wsCloseHandler?.handleClose(ctx) }
+        tryAfterHandlers(ctx) { it.handlers.wsCloseHandler?.handleClose(ctx) }
         wsLogger?.wsCloseHandler?.handleClose(ctx)
-        sessionIds.remove(session) // the socket has been closed, we no longer need to keep track of the session ID
     }
 
     @OnWebSocketError
     fun onError(session: Session, throwable: Throwable?) {
-        val ctx = WsErrorContext(sessionIds[session]!!, session, throwable)
-        tryBeforeAndEndpointHandlers(ctx) { it.handler.wsErrorHandler?.handleError(ctx) }
-        tryAfterHandlers(ctx) { it.handler.wsErrorHandler?.handleError(ctx) }
+        val ctx = WsErrorContext(sessionId, session, throwable)
+        tryBeforeAndEndpointHandlers(ctx) { it.handlers.wsErrorHandler?.handleError(ctx) }
+        tryAfterHandlers(ctx) { it.handlers.wsErrorHandler?.handleError(ctx) }
         wsLogger?.wsErrorHandler?.handleError(ctx)
     }
 
diff --git a/javalin/src/main/java/io/javalin/websocket/WsExceptionHandler.java b/javalin/src/main/java/io/javalin/websocket/WsExceptionHandler.java
index 39e1ee56..f5d0d9fc 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsExceptionHandler.java
+++ b/javalin/src/main/java/io/javalin/websocket/WsExceptionHandler.java
@@ -10,7 +10,7 @@ import org.jetbrains.annotations.NotNull;
 
 /**
  * A handler for use with {@link io.javalin.Javalin#wsException(Class, WsExceptionHandler)}.
- * Is triggered when an exception is thrown by a {@link WsHandler}.
+ * Is triggered when an exception is thrown by a {@link WsHandlers}.
  *
  * @see WsContext
  */
diff --git a/javalin/src/main/java/io/javalin/websocket/WsHandler.java b/javalin/src/main/java/io/javalin/websocket/WsHandlers.java
similarity index 93%
rename from javalin/src/main/java/io/javalin/websocket/WsHandler.java
rename to javalin/src/main/java/io/javalin/websocket/WsHandlers.java
index 63333813..9239887f 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsHandler.java
+++ b/javalin/src/main/java/io/javalin/websocket/WsHandlers.java
@@ -8,7 +8,10 @@ package io.javalin.websocket;
 
 import org.jetbrains.annotations.NotNull;
 
-public class WsHandler {
+/**
+ * Holds the different WebSocket handlers for a specific {@link WsEntry} or the WebSocket logger.
+ */
+public class WsHandlers {
 
     WsConnectHandler wsConnectHandler = null;
     WsMessageHandler wsMessageHandler = null;
diff --git a/javalin/src/main/java/io/javalin/websocket/WsPathMatcher.kt b/javalin/src/main/java/io/javalin/websocket/WsPathMatcher.kt
index d90ea4b8..53b45155 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsPathMatcher.kt
+++ b/javalin/src/main/java/io/javalin/websocket/WsPathMatcher.kt
@@ -10,7 +10,7 @@ import io.javalin.core.PathParser
 import io.javalin.core.security.Role
 import java.util.*
 
-data class WsEntry(val type: WsHandlerType, val path: String, val ignoreTrailingSlashes: Boolean, val handler: WsHandler, val permittedRoles: Set<Role>) {
+data class WsEntry(val type: WsHandlerType, val path: String, val ignoreTrailingSlashes: Boolean, val handlers: WsHandlers, val permittedRoles: Set<Role>) {
     private val pathParser = PathParser(path, ignoreTrailingSlashes)
     fun matches(path: String) = pathParser.matches(path)
     fun extractPathParams(path: String) = pathParser.extractPathParams(path)


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestCompression.kt b/javalin/src/test/java/io/javalin/TestCompression.kt
index 0415d92c..4f447b47 100644
--- a/javalin/src/test/java/io/javalin/TestCompression.kt
+++ b/javalin/src/test/java/io/javalin/TestCompression.kt
@@ -149,8 +149,6 @@ class TestCompression {
         assertThat(getResponse(http.origin, "/html.html", "br, gzip").headers().get(Header.CONTENT_ENCODING)).isEqualTo("gzip")
     }
 
-    /* Test for backwards compatibility. Ensures that the old dynamicGzip boolean is respected
-       when a CompressionStrategy is not set */
     @Test
     fun `does gzip when CompressionStrategy not set`() {
         val defaultApp = Javalin.create {
diff --git a/javalin/src/test/java/io/javalin/TestConfiguration.kt b/javalin/src/test/java/io/javalin/TestConfiguration.kt
index 915099d8..90b63fb0 100644
--- a/javalin/src/test/java/io/javalin/TestConfiguration.kt
+++ b/javalin/src/test/java/io/javalin/TestConfiguration.kt
@@ -33,7 +33,6 @@ class TestConfiguration {
             it.autogenerateEtags = true
             it.contextPath = "/"
             it.defaultContentType = "text/plain"
-            it.dynamicGzip = true
             it.enableCorsForAllOrigins()
             it.enableDevLogging()
             it.registerPlugin(RouteOverviewPlugin("/test", roles()))
@@ -41,7 +40,6 @@ class TestConfiguration {
             it.enforceSsl = true
             it.logIfServerNotStarted = false
             it.prefer405over404 = false
-            it.requestCacheSize = 8192L
             it.requestLogger { ctx, executionTimeMs -> }
             it.sessionHandler { SessionHandler() }
             // WsServlet
@@ -83,13 +81,4 @@ class TestConfiguration {
         assertThat(app.config.inner.compressionStrategy.gzip?.level).isEqualTo(2)
         assertThat(app.config.inner.compressionStrategy.brotli).isNull()
     }
-
-    @Test
-    fun `compression strategy gets disabled when dynamicGzip is set to false`() {
-        val app = Javalin.create {
-            it.dynamicGzip = false
-            it.compressionStrategy(null, Gzip(8))
-        }
-        assertThat(app.config.inner.compressionStrategy).isEqualTo(CompressionStrategy.NONE)
-    }
 }
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVue.kt b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
index 2be3d8a9..7cfcb93e 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVue.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
@@ -137,7 +137,7 @@ class TestJavalinVue {
 
     @Test
     fun `setting rootDirectory with Path works`() = TestUtil.test { app, http ->
-        JavalinVue.rootDirectory(Paths.get("src/test/resources/vue"))
+        JavalinVue.rootDirectory = Paths.get("src/test/resources/vue")
         app.get("/path", VueComponent("test-component"))
         assertThat(http.getBody("/path")).contains("<test-component></test-component>")
     }
diff --git a/javalin/src/test/java/io/javalin/TestMaxRequestSize.kt b/javalin/src/test/java/io/javalin/TestMaxRequestSize.kt
index 1f387661..639ec5f1 100644
--- a/javalin/src/test/java/io/javalin/TestMaxRequestSize.kt
+++ b/javalin/src/test/java/io/javalin/TestMaxRequestSize.kt
@@ -1,24 +1,41 @@
 package io.javalin
 
 import io.javalin.testing.TestUtil
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.ByteArrayEntity
+import org.apache.http.impl.client.HttpClients
+import org.apache.http.util.EntityUtils
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
 
 class TestMaxRequestSize {
 
     @Test
-    fun `if max request size unset accept any content size`() = TestUtil.test { app, http ->
-        app.post("/max-content-size") { ctx -> ctx.result(ctx.req.inputStream) }
-        val httpResponse = http.post("/max-content-size").body(ByteArray(100000)).asString()
-        assertThat(httpResponse.status).isEqualTo(200)
+    fun `max request size is set by default`() = TestUtil.test { app, http ->
+        app.post("/") { ctx -> ctx.result(ctx.body()) }
+        assertThat(http.post("/").body(ByteArray(1_000_000)).asString().status).isEqualTo(200)
+        assertThat(http.post("/").body(ByteArray(1_000_001)).asString().status).isEqualTo(413)
     }
 
     @Test
-    fun `if content is bigger than max request size return 413`() = TestUtil.test(Javalin.create {
-        it.maxRequestSize = 0L
-    }) { app, http ->
-        app.post("/max-content-size") { ctx -> ctx.result(ctx.req.inputStream) }
-        val httpResponse = http.post("/max-content-size").body("body").asString()
-        assertThat(httpResponse.status).isEqualTo(413)
+    fun `user can configure max request size`() = TestUtil.test(Javalin.create { it.maxRequestSize = 4L }) { app, http ->
+        app.post("/") { ctx -> ctx.result(ctx.body()) }
+        assertThat(http.post("/").body(ByteArray(4)).asString().status).isEqualTo(200)
+        assertThat(http.post("").body(ByteArray(5)).asString().status).isEqualTo(413)
     }
+
+    @Test
+    fun `body can be read multiple times`() = TestUtil.test(Javalin.create()) { app, http ->
+        app.post("/") { ctx -> ctx.result(ctx.body() + ctx.body() + ctx.body()) }
+        assertThat(http.post("/").body("Hi").asString().body).isEqualTo("HiHiHi")
+    }
+
+    @Test
+    fun `can read payloads larger than max size by using inputstream`() = TestUtil.test(Javalin.create { it.maxRequestSize = 4L }) { app, http ->
+        app.post("/body") { ctx -> ctx.result(ctx.body()) }
+        assertThat(http.post("/body").body("123456").asString().body).isEqualTo("Payload too large")
+        app.post("/stream") { ctx -> ctx.result(ctx.req.inputStream.readBytes()) }
+        assertThat(http.post("/stream").body("123456").asString().body).isEqualTo("123456")
+    }
+
 }
diff --git a/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt b/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt
index 14dc6f7e..24d694a2 100644
--- a/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt
+++ b/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt
@@ -24,14 +24,14 @@ class TestRateLimitUtil {
 
     @Test
     fun `rate limiting kicks in if number of requests exceeds rate limit`() = TestUtil.test(testApp) { app, http ->
-        repeat(20) { http.get("/") }
+        repeat(50) { http.get("/") }
         assertThat(http.get("/").status).isEqualTo(429)
         assertThat(http.get("/").body).isEqualTo("Rate limit exceeded - Server allows 5 requests per hour.")
     }
 
     @Test
     fun `both path and HTTP method must match for rate limiting to kick in`() = TestUtil.test(testApp) { app, http ->
-        repeat(20) { http.get("/") }
+        repeat(50) { http.get("/") }
         assertThat(http.get("/").status).isEqualTo(429)
         assertThat(http.get("/test").status).isNotEqualTo(429)
         assertThat(http.post("/").asString().status).isNotEqualTo(429)
@@ -39,9 +39,9 @@ class TestRateLimitUtil {
 
     @Test
     fun `rate limit on dynamic path-params limits per endpoint, not per URL`() = TestUtil.test(testApp) { app, http ->
-        repeat(10) { http.get("/dynamic/1") }
-        repeat(10) { http.get("/dynamic/2") }
-        repeat(10) { http.get("/dynamic/3") }
+        repeat(50) { http.get("/dynamic/1") }
+        repeat(50) { http.get("/dynamic/2") }
+        repeat(50) { http.get("/dynamic/3") }
         assertThat(http.get("/dynamic/4").status).isEqualTo(429)
         assertThat(http.get("/dynamic/5").body).isEqualTo("Rate limit exceeded - Server allows 5 requests per minute.")
     }
diff --git a/javalin/src/test/java/io/javalin/TestRequestCache.kt b/javalin/src/test/java/io/javalin/TestRequestCache.kt
deleted file mode 100644
index 2352d975..00000000
--- a/javalin/src/test/java/io/javalin/TestRequestCache.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package io.javalin
-
-import io.javalin.testing.TestUtil
-import org.apache.http.client.methods.HttpPost
-import org.apache.http.entity.ByteArrayEntity
-import org.apache.http.impl.client.HttpClients
-import org.apache.http.util.EntityUtils
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Test
-
-class TestRequestCache {
-
-    @Test
-    fun `request caching request does not drain InputStream`() = TestUtil.test { app, http ->
-        app.post("/cache-chunked-encoding") { ctx -> ctx.result(ctx.req.inputStream) }
-        val body = ByteArray(10000).mapIndexed { i, _ -> (i % 256).toByte() }.toByteArray()
-        val post = HttpPost(http.origin + "/cache-chunked-encoding").apply {
-            entity = ByteArrayEntity(body).apply { isChunked = true }
-        }
-        val response = HttpClients.createDefault().execute(post)
-        val result = EntityUtils.toByteArray(response.entity)
-        assertThat(body).isEqualTo(result)
-        response.close()
-    }
-
-    @Test
-    fun `disabling request-caching works`() = TestUtil.test(Javalin.create { it.requestCacheSize = 0 }) { app, http ->
-        app.post("/disabled-cache") { ctx ->
-            ctx.result(ctx.body())
-        }
-        val response = http.post("/disabled-cache").body("test").asString()
-        assertThat(response.status).isEqualTo(200)
-        assertThat(response.body).isEqualTo("test")
-    }
-}
diff --git a/javalin/src/test/java/io/javalin/routeoverview/VisualTest.java b/javalin/src/test/java/io/javalin/routeoverview/VisualTest.java
index ea589510..6a5b234d 100644
--- a/javalin/src/test/java/io/javalin/routeoverview/VisualTest.java
+++ b/javalin/src/test/java/io/javalin/routeoverview/VisualTest.java
@@ -11,7 +11,7 @@ import io.javalin.core.util.RouteOverviewPlugin;
 import io.javalin.http.Context;
 import io.javalin.http.Handler;
 import io.javalin.http.HandlerType;
-import io.javalin.websocket.WsHandler;
+import io.javalin.websocket.WsHandlers;
 import static io.javalin.TestAccessManager.MyRoles.ROLE_ONE;
 import static io.javalin.TestAccessManager.MyRoles.ROLE_THREE;
 import static io.javalin.TestAccessManager.MyRoles.ROLE_TWO;
@@ -74,8 +74,8 @@ public class VisualTest {
         });
     }
 
-    private static void wsMethodRef(WsHandler wsHandler) {
-        wsHandler.onConnect(ctx -> ctx.session.getRemote().sendString("Connected!"));
+    private static void wsMethodRef(WsHandlers wsHandlers) {
+        wsHandlers.onConnect(ctx -> ctx.session.getRemote().sendString("Connected!"));
     }
 
     private static void methodReference(Context context) {