Repository: tipsy/javalin
Pull request #1171 (2021-02-16 22:28:28)

Fix for autogenerated etags - make them work even if input stream does not support mark/reset
For bug discussed in issue #1170 
PR contains a fix for the bug, plus test (test is failing without the bugfix applied, and passed after applying the fix)

tipsy: Nice catch, thank you @grzegorzbor ! 

Patch:
diff --git a/javalin/src/main/java/io/javalin/core/util/Util.kt b/javalin/src/main/java/io/javalin/core/util/Util.kt
index 2c6a9f56..bbbe672c 100644
--- a/javalin/src/main/java/io/javalin/core/util/Util.kt
+++ b/javalin/src/main/java/io/javalin/core/util/Util.kt
@@ -127,10 +127,13 @@ object Util {
           |""".trimMargin())
     }
 
-    fun getChecksumAndReset(inputStream: InputStream): String {
+    fun getChecksumAndReset(inputStream: ByteArrayInputStream): String {
+        inputStream.mark(Int.MAX_VALUE) //it's all in memory so there is no readAheadLimit
         val cis = CheckedInputStream(inputStream, Adler32())
-        val out = ByteArrayOutputStream()
-        cis.copyTo(out)
+        var byte = cis.read()
+        while (byte > -1) {
+            byte = cis.read()
+        }
         inputStream.reset()
         return cis.checksum.value.toString()
     }
diff --git a/javalin/src/main/java/io/javalin/http/JavalinResponseWrapper.kt b/javalin/src/main/java/io/javalin/http/JavalinResponseWrapper.kt
index 8dcd85ad..35d5d047 100644
--- a/javalin/src/main/java/io/javalin/http/JavalinResponseWrapper.kt
+++ b/javalin/src/main/java/io/javalin/http/JavalinResponseWrapper.kt
@@ -35,16 +35,26 @@ class JavalinResponseWrapper(val res: HttpServletResponse, private val rwc: Resp
 
     fun write(resultStream: InputStream?) {
         if (resultStream == null) return
+        var inputStream = resultStream
         if (res.getHeader(ETAG) != null || (rwc.config.autogenerateEtags && rwc.type == HandlerType.GET)) {
-            val serverEtag = res.getHeader(ETAG) ?: Util.getChecksumAndReset(resultStream) // calculate if not set
+            val serverEtag: String
+            if (res.getHeader(ETAG) != null) {
+                serverEtag = res.getHeader(ETAG)
+            } else {
+                //we must load and wrap the whole input stream into memory in order to generate the etag based on the whole content
+                inputStream = resultStream.readBytes().inputStream()
+                resultStream.close() //close original stream, it was already copied to byte array
+                serverEtag = Util.getChecksumAndReset(inputStream)
+            }
             res.setHeader(ETAG, serverEtag)
             if (serverEtag == rwc.clientEtag) {
                 res.status = 304
+                inputStream.close()
                 return // don't write body
             }
         }
-        resultStream.copyTo(outputStreamWrapper)
-        resultStream.close()
+        inputStream.copyTo(outputStreamWrapper)
+        inputStream.close()
         outputStreamWrapper.finalize()
     }
 


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestEtags.kt b/javalin/src/test/java/io/javalin/TestEtags.kt
index 668f9777..c4f3f7d8 100644
--- a/javalin/src/test/java/io/javalin/TestEtags.kt
+++ b/javalin/src/test/java/io/javalin/TestEtags.kt
@@ -38,6 +38,16 @@ class TestEtags {
         assertThat(etag).isEqualTo(etag2)
     }
 
+    @Test
+    fun `autogenerated etags work for input stream response`() = TestUtil.test(etagsEnabledApp) { app, http ->
+        app.get("/inputStreamResult") { ctx -> ctx.result(javaClass.getResourceAsStream("/public/file")) }
+        val response = Unirest.get(http.origin + "/inputStreamResult").asString()
+        assertThat(response.status).isEqualTo(200)
+        assertThat(response.body).isEqualTo("TESTFILE")
+        val etag = response.headers.getFirst(Header.ETAG)
+        assertThat(etag).isNotBlank()
+    }
+
     @Test
     fun `autogenerated etags are not added to PUT request`() = TestUtil.test(etagsEnabledApp) { app, http ->
         app.put("/automatic") { ctx -> ctx.result("Hello!") }