Repository: tipsy/javalin
Pull request #1275 (2021-06-10 21:30:17)

[context] Rework future handling
This PR was sparked by https://github.com/tipsy/javalin/issues/1273 - I thought it would be neat if we could have an `ctx.async` method with a future scope that let you use the `Context` as normal:

```kotlin
fun async(runnable: ThrowingRunnable) {
    ...
    resultFuture = CompletableFuture.supplyAsync { runnable.run() }
}
```

Usage:

```kotlin
ctx.async {
    val futureValue = myFuture.get()
    if (futureValue == null) {
        ctx.status(204)
    } else {
        ctx.json(futureValue)
    }
}
```

Seeing as I never use async, I have no idea if this is a good idea.

Edit: It wasn't a good idea. I've made a new suggestion:

```
class AsyncContext(private val syncContext: Context) {

    private var result: CompletableFuture<*>? = null

    fun result() = result

    fun result(future: CompletableFuture<*>): AsyncContext {
        result = future
        return this
    }

    fun json(future: CompletableFuture<*>): AsyncContext = result(future.thenApply {
        if (it != null) JavalinJson.toJson(it).also { syncContext.contentType("application/json") } else ""
    })

}
```

It's challenging to work on something you don't understand very well, but I think we're getting somewhere.

### Edit: The current PR now has this API:

```kotlin
app.get("/") { ctx ->
    val hasContent = ctx.queryParam("has-content") != null
    ctx.future(getFuture("some-future-result")) { result ->
        if (hasContent) {
            ctx.status(200)
            ctx.json(result!!)
        } else {
            ctx.status(204)
        }
    }
}
```

And there is a default implementation of the callback for when you just do `ctx.future(future)`, which is:

```kotlin
futureConsumer = callback ?: Consumer { result ->
    when (result) {
        is InputStream -> result(result)
        is String -> result(result)
        is Any -> json(result)
    }
}
```

tipsy: @LoonyRules please have a look.

Pinging some other people who have worked on/with async: @bchristenson @dancohensmith @wilerson

I'm out of my element here, I'm just trying to make the API more consistent 😄 

dzikoysk: I don't quite like it, because it's limited to the default thread pool shared between whole jvm instance. `supplyAsync` uses global forkjoinpool, which is a small fixed pool (number of cpus):

* https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html#commonPool--

In my opinion CompletableFuture should be always used within the dedicated executor service. You can not only just control this, but you can also limit strange performance issues caused by such management. Let's say that someone used `supplyAsync` to perform some IO operations to e.g. load files from disk etc. By using the same pool, the async request may become kinda freezed, because some random code in another place is doing sth.

I'm thinking about an alternative. In general I think that `.async(executorService) { }` might be an option for now. We may use default forkjoinpool if user won't specify it.
tipsy: Wasn't aware that `.get()` is blocking, so that's fun (I guess that's what I ...get for not reading up on any of this)

Not sure if worth looking into: https://github.com/electronicarts/ea-async

The main intent of this PR isn't to provide a solution, but rather start a discussion on how we can improve this part of Javalin.
LoonyRules: > I like it, but it should be a little improved, because it's limited to the default thread pool shared between whole jvm instance. `supplyAsync` uses global forkjoinpool, which is a small fixed pool (number of cpus):
> 
> * https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html#commonPool--
> 
> In my opinion CompletableFuture should be always used within the dedicated executor service. You can not only just control this, but you can also limit strange performance issues caused by such management. Let's say that someone used `supplyAsync` to perform some IO operations to e.g. load files from disk etc. By using the same pool, the async request may become kinda freezed, because some random code in another place is doing sth.
> 
> I'm thinking about an alternative. In general I think that `.async(executorService) { }` might be an option for now. We may use default forkjoinpool if user won't specify it.

In regards to what was mentioned in here I agree roughly on the idea of what this requests.

- Javalin could use the default QueuedThreadPool / ExecutorService (QTP/ES) that the Javalin Server uses for these async requests by default.
- The Javalin Config could allow for us to define our own QTP/ES just in case we want to separate the incoming handling pool away from the async handling pool.
- Implement the `ctx.async()` method that uses the default QTP/ES.
- But also implement a `ctx.async(executorService)` method that then allows users to define a different one just in case someone somewhere ever needs to quickly switch for whatever reason.

But generally speaking -- I would be concerned with moving any async functionality away from a future and into what is essentially a Runnable managed by a thread pool of some kind so if both methods are possible that would be great. Future's for developers that are knowledgeable with them and then QTP/ES for developers that just generally want to run something async and not in Future form.

A lot of IO api's these days have methods laid out something like this to allow for blocking thread calls and future support:
```java

public Document getDocument(ObjectId objectId) {
  return this.mongo.find(Filters.eq("_id", objectId)).first();
}

public CompletableFuture<Document> getDocumentFuture(ObjectId objectId) {
  return CompletableFuture.supplyAsync(() -> {
    return this.getDocument(objectId);
  }, DATABASE_EXECUTOR_SERVICE); // DATABASE_EXECUTOR_SERVICE being optional of course.
}
```

So whilst removing Future support in replacement of `async()` would allow you to do the same thing, what you are removing support wise is the implementation functionality of Futures in general.

**Examples:**

1. Generate a string async then on the same thread it was generated on, pass that through for a final handling. In our case, print it into the console: 
```java
CompletableFuture
  .supplyAsync(() -> "Hello World!")
  .thenAccept(input -> System.out.println("Future response: " + input));
````

2. Get multiple documents from the Database and when we have all of them, handle our request.
```java
ctx -> {
  // Note: This is a util method I have that just does CompletableFuture.allOf(...) and joins the results
  //       together so when we do thenAccept(...) we have all of our results in hand. By default allOf(...)
  //       returns a Void generic type so you can't handle the responses together.
  FutureUtil
    .asList(
      ctx.queryParams("id")
        .stream()
        .map(id -> getDocumentFuture(new ObjectId(id)))
        .collect(Collectors.toList())
    )
    // Once all of our futures are completed. For the sakes of example we do multiple queries to get 
    // documents but in the real world doing 1 query with OR filters would be what you would want. This 
    // was just the quickest example.
    .thenAccept(documents -> {
      for (Document document : documents) { 
        System.out.println("Document: " + document); 
      }
    })
    // When there's an error
    .exceptionally(throwable -> {
      throwable.printStackTrace();
      return null; // Void return type requirement - don't fret.
    });
}
```
LoonyRules: > Wasn't aware that `.get()` is blocking, so that's fun (I guess that's what I ...get for not reading up on any of this)
> 
> Not sure if worth looking into: https://github.com/electronicarts/ea-async

Javalin does not need that library for functionality of this.

> ```kotlin
> ctx.async {
>    val futureValue = myFuture.get()
>   if (futureValue == null) {
>        ctx.status(204)
>    } else {
>        ctx.json(futureValue)
>    }
> }

Following on from the knowledge of `.get()` blocking the thread that called it. The proposed usage would mean that each async request could be using 2/3 threads and blocking all until completion.

1) The Javalin Server is using a thread for the incoming request. Jetty then creates the Context and passes it onto the handler.
2) `ctx.async()` from what I understand is doing another `CompletableFuture.supplyAsync()` method.
3) Depending on the developer, the `myFuture` instantiation could be done via `CompletableFuture.supplyAsync(Supplier)` so now we have 3 threads for a single request.

This would obviously not be recommended. When Javalin has a Context that is marked as async, whether that be the `.async()` method or a `.status(Future)` method of some kind, the 1st thread should be freed up and put back into the thread pool to handle more requests.

If Javalin gets given a Future you can just do:

```
ctx.future != null
  future.thenAccept(input -> {
    // Handle the response - in our example this is the Status Code we want to set for the response.
  }).exceptionally(throwable -> {
    // Error handling goes here
  });
```
tipsy: Just some quick notes on alternatives: 

```kotlin
api.get("/path") {
    ctx.async { futureCtx ->
        futureCtx.result(obj)
        futureCtx.json(obj)
        futureCtx.status(int) // not sure why this one is needed
    }
}
```

```kotlin
api.get("/path") {
    val asynxContext = ctx.async()
    asynxContext.result(obj)
    asynxContext.json(obj)
    asynxContext.status(int) // not sure why this one is needed
}
```

Patch:
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
index 5c53af1b..db2e7357 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
@@ -28,7 +28,7 @@ class GraphQLHandler(val options: GraphQLOptions) {
         val result = this.genericExecute(body)
                 .execute()
                 .thenApplyAsync { JavalinJson.toJson(it) }
-        ctx.contentType("application/json").result(result)
+        ctx.contentType("application/json").future(result)
     }
 
     fun execute(ctx: WsMessageContext) {
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt
index 7616431c..12d4976d 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/jackson/JavalinModelResolver.kt
@@ -19,8 +19,10 @@ class JavalinModelResolver(mapper: ObjectMapper) : ModelResolver(mapper) {
         }
         val type = extractJavaType(annotatedType)
 
-        if (type.isTypeOrSubTypeOf(Instant::class.java) && _mapper.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) {
-            return PrimitiveType.LONG.createProperty();
+        if (type.isTypeOrSubTypeOf(Instant::class.java) && _mapper.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+            && annotatedType.ctxAnnotations.isEmpty()
+        ) {
+            return PrimitiveType.LONG.createProperty()
         }
 
         return super.resolve(annotatedType, context, next)
diff --git a/javalin/src/main/java/io/javalin/http/Context.kt b/javalin/src/main/java/io/javalin/http/Context.kt
index 9681e3db..faee3236 100644
--- a/javalin/src/main/java/io/javalin/http/Context.kt
+++ b/javalin/src/main/java/io/javalin/http/Context.kt
@@ -24,6 +24,7 @@ import java.io.InputStream
 import java.nio.charset.Charset
 import java.util.*
 import java.util.concurrent.CompletableFuture
+import java.util.function.Consumer
 import javax.servlet.http.Cookie
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
@@ -49,6 +50,7 @@ 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
+    internal var futureConsumer: Consumer<Any?>? = null
     private val body by lazy {
         this.throwPayloadTooLargeIfPayloadTooLarge()
         req.inputStream.readBytes()
@@ -359,19 +361,21 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /** Gets the current context result as an [InputStream] (if set). */
     fun resultStream(): InputStream? = resultStream
 
-    /**
-     * Sets context result to the specified CompletableFuture<String>
-     * or CompletableFuture<InputStream>.
-     * Will overwrite the current result if there is one.
-     * Can only be called inside endpoint handlers (ones representing HTTP verbs).
-     */
-    fun result(future: CompletableFuture<*>): Context {
+    @JvmOverloads
+    fun future(future: CompletableFuture<*>, callback: Consumer<Any?>? = null): Context {
+        if (!handlerType.isHttpMethod() || inExceptionHandler) {
+            throw IllegalStateException("You can only set CompletableFuture results in endpoint handlers.")
+        }
         resultStream = null
-        if (handlerType.isHttpMethod() && !inExceptionHandler) {
-            this.resultFuture = future
-            return this
+        resultFuture = future
+        futureConsumer = callback ?: Consumer { result ->
+            when (result) {
+                is InputStream -> result(result)
+                is String -> result(result)
+                is Any -> json(result)
+            }
         }
-        throw IllegalStateException("You can only set CompletableFuture results in endpoint handlers.")
+        return this
     }
 
     /** Gets the current context result as a [CompletableFuture] (if set). */
@@ -447,17 +451,6 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
         return contentType("application/json").result(JavalinJson.toJson(obj))
     }
 
-    /**
-     * Serializes the object resulting from the completion of the given future
-     * to a JSON-string using JavalinJson and sets it as the context result.
-     * Sets content type to application/json.
-     *
-     * JavalinJson can be configured to use any mapping library.
-     */
-    fun json(future: CompletableFuture<*>) = result(future.thenApply {
-        if (it != null) JavalinJson.toJson(it).also { contentType("application/json") } else ""
-    })
-
     /**
      * Renders a file with specified values and sets it as the context result.
      * Also sets content-type to text/html.
diff --git a/javalin/src/main/java/io/javalin/http/ExceptionMapper.kt b/javalin/src/main/java/io/javalin/http/ExceptionMapper.kt
index fba55eb5..2d3f2b27 100644
--- a/javalin/src/main/java/io/javalin/http/ExceptionMapper.kt
+++ b/javalin/src/main/java/io/javalin/http/ExceptionMapper.kt
@@ -8,6 +8,8 @@ package io.javalin.http
 
 import io.javalin.core.util.JavalinLogger
 import io.javalin.core.util.Util
+import java.util.concurrent.CompletionException
+import javax.servlet.http.HttpServletResponse
 
 class ExceptionMapper {
 
@@ -35,6 +37,21 @@ class ExceptionMapper {
         handle(e, ctx)
     }
 
+    internal fun handleFutureException(ctx: Context, throwable: Throwable): Nothing? {
+        if (throwable is CompletionException && throwable.cause is Exception) {
+            handle(throwable.cause as Exception, ctx)
+        } else if (throwable is Exception) {
+            handle(throwable, ctx)
+        }
+        return null
+    }
+
+    internal fun handleUnexpectedThrowable(res: HttpServletResponse, throwable: Throwable) {
+        if (Util.isClientAbortException(throwable)) return // aborts can be ignored
+        res.status = 500
+        JavalinLogger.error("Exception occurred while servicing http-request", throwable)
+    }
+
     private fun noUserHandler(e: Exception) =
             this.handlers[e::class.java] == null && this.handlers[HttpResponseException::class.java] == null
 }
diff --git a/javalin/src/main/java/io/javalin/http/JavalinServlet.kt b/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
index c2ecf9b2..225d1682 100644
--- a/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
+++ b/javalin/src/main/java/io/javalin/http/JavalinServlet.kt
@@ -10,13 +10,9 @@ import io.javalin.core.JavalinConfig
 import io.javalin.core.security.RouteRole
 import io.javalin.core.util.CorsPlugin
 import io.javalin.core.util.Header
-import io.javalin.core.util.JavalinLogger
 import io.javalin.core.util.LogUtil
-import io.javalin.core.util.Util
 import io.javalin.http.util.ContextUtil
 import io.javalin.http.util.MethodNotAllowedUtil
-import java.io.InputStream
-import java.util.concurrent.CompletionException
 import javax.servlet.http.HttpServlet
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
@@ -64,63 +60,44 @@ class JavalinServlet(val config: JavalinConfig) : HttpServlet() {
                 throw NotFoundResponse()
             }
 
-            fun tryErrorHandlers() = tryWithExceptionMapper {
-                errorMapper.handle(ctx.status(), ctx)
-            }
-
-            fun tryAfterHandlers() = tryWithExceptionMapper {
-                matcher.findEntries(HandlerType.AFTER, requestUri).forEach { entry ->
-                    entry.handler.handle(ContextUtil.update(ctx, entry, requestUri))
+            fun finishUpResponse(httpServletResponse: HttpServletResponse) {
+                tryWithExceptionMapper { // run error mappers (can mutate ctx)
+                    errorMapper.handle(ctx.status(), ctx)
                 }
+                tryWithExceptionMapper { // run after handlers (can mutate ctx)
+                    matcher.findEntries(HandlerType.AFTER, requestUri).forEach { entry ->
+                        entry.handler.handle(ContextUtil.update(ctx, entry, requestUri))
+                    }
+                }
+                JavalinResponseWrapper(httpServletResponse, rwc).write(ctx.resultStream()) // write the response
+                config.inner.requestLogger?.handle(ctx, LogUtil.executionTimeMs(ctx)) // log stuff
             }
 
             LogUtil.setup(ctx, matcher, config.inner.requestLogger != null) // start request lifecycle
             ctx.header(Header.SERVER, "Javalin")
             ctx.contentType(config.defaultContentType)
             tryBeforeAndEndpointHandlers()
-            if (ctx.resultFuture() == null) { // finish request synchronously
-                tryErrorHandlers()
-                tryAfterHandlers()
-                JavalinResponseWrapper(res, rwc).write(ctx.resultStream())
-                config.inner.requestLogger?.handle(ctx, LogUtil.executionTimeMs(ctx))
-            } else { // finish request asynchronously
-                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)
-                    } else if (throwable is Exception) {
-                        exceptionMapper.handle(throwable, ctx)
-                    }
-                    null
-                }.thenAccept {
-                    when (it) {
-                        is InputStream -> ctx.result(it)
-                        is String -> ctx.result(it)
-                    }
-                    tryErrorHandlers()
-                    tryAfterHandlers()
-                    val asyncRes = asyncContext.response as HttpServletResponse
-                    JavalinResponseWrapper(asyncRes, rwc).write(ctx.resultStream())
-                    config.inner.requestLogger?.handle(ctx, LogUtil.executionTimeMs(ctx))
-                    asyncContext.complete() // async lifecycle complete
-                }.exceptionally { t ->
-                    handleUnexpectedThrowable(t, res)
-                    asyncContext.complete() // async lifecycle complete
-                    null
-                }
+            if (ctx.resultFuture() == null) {
+                return finishUpResponse(res) // request lifecycle is complete (blocking/synchronous)
+            }
+            // user called Context#future, we call startAsync and setup callbacks
+            val asyncContext = req.startAsync().apply { timeout = config.asyncRequestTimeout }
+            ctx.resultFuture()!!.exceptionally { throwable ->
+                exceptionMapper.handleFutureException(ctx, throwable)
+            }.thenAccept { futureValue ->
+                ctx.futureConsumer?.accept(futureValue) // this consumer can set result, status, etc
+                finishUpResponse(asyncContext.response as HttpServletResponse)
+                asyncContext.complete()
+            }.exceptionally { throwable ->
+                exceptionMapper.handleUnexpectedThrowable(res, throwable) // exception might occur when writing response
+                asyncContext.complete()
+                null
             }
-            Unit // return void
-        } catch (t: Throwable) {
-            handleUnexpectedThrowable(t, res)
+        } catch (throwable: Throwable) {
+            exceptionMapper.handleUnexpectedThrowable(res, throwable)
         }
     }
 
-    private fun handleUnexpectedThrowable(t: Throwable, res: HttpServletResponse) {
-        if (Util.isClientAbortException(t)) return // aborts can be ignored
-        res.status = 500
-        JavalinLogger.error("Exception occurred while servicing http-request", t)
-    }
-
     private fun hasGetHandlerMapped(requestUri: String) = matcher.findEntries(HandlerType.GET, requestUri).isNotEmpty()
 
     fun addHandler(handlerType: HandlerType, path: String, handler: Handler, roles: Set<RouteRole>) {


Test patch:
diff --git a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt
index f84dd828..4d0a3175 100644
--- a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt
+++ b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApi.kt
@@ -34,6 +34,7 @@ import io.javalin.plugin.openapi.dsl.documentedContent
 import io.javalin.plugin.openapi.dsl.oneOf
 import io.javalin.plugin.openapi.jackson.JacksonToJsonMapper
 import io.javalin.testing.TestUtil
+import io.swagger.v3.oas.annotations.media.Schema
 import io.swagger.v3.oas.models.OpenAPI
 import io.swagger.v3.oas.models.info.Info
 import io.swagger.v3.oas.models.security.SecurityRequirement
@@ -43,6 +44,7 @@ import org.assertj.core.api.Assertions.assertThat
 import org.assertj.core.api.Assertions.assertThatExceptionOfType
 import org.junit.Test
 import java.time.Instant
+import java.util.*
 
 class Address(val street: String, val number: Int)
 
@@ -50,6 +52,9 @@ class User(val name: String, val address: Address? = null, val userType: UserTyp
 
 class Log(val timestamp: Instant, val message: String)
 
+class AnnotatedLog(@Schema(type = "string", example = "test example") val timestamp: Instant, val message: String)
+
+
 enum class UserType {
     ONE, TWO
 }
@@ -105,57 +110,57 @@ fun buildComplexExample(options: OpenApiOptions): Javalin {
     }
 
     val getUserDocumentation = document()
-            .operation {
-                it.description = "Get a specific user"
-                it.summary = "Get current user"
-                it.operationId = "getCurrentUser"
-                it.deprecated = true
-                it.addTagsItem("user")
-            }
-            .json<User>("200") {
-                it.description = "Request successful"
-            }
-            .result("200", documentedContent<User>("application/xml"))
+        .operation {
+            it.description = "Get a specific user"
+            it.summary = "Get current user"
+            it.operationId = "getCurrentUser"
+            it.deprecated = true
+            it.addTagsItem("user")
+        }
+        .json<User>("200") {
+            it.description = "Request successful"
+        }
+        .result("200", documentedContent<User>("application/xml"))
     app.get("/user", documented(getUserDocumentation) {
         it.json(User(name = "Jim"))
     })
 
     val getUserDocumentationSpecific = document()
-            .operation {
-                it.description = "Get a specific user with his/her id"
-                it.summary = "Get specific user"
-                it.operationId = "getSpecificUser"
-            }
-            .json<User>("200") {
-                it.description = "Request successful"
-            }
-            .result("200", documentedContent<User>("application/xml"))
+        .operation {
+            it.description = "Get a specific user with his/her id"
+            it.summary = "Get specific user"
+            it.operationId = "getSpecificUser"
+        }
+        .json<User>("200") {
+            it.description = "Request successful"
+        }
+        .result("200", documentedContent<User>("application/xml"))
 
     app.get("/user/:userid", documented(getUserDocumentationSpecific) {
         it.json(User(name = it.pathParam("userid")))
     })
 
     val getUsersDocumentation = document()
-            .operation {
-                it.addTagsItem("user")
-            }
-            .cookie<String>("my-cookie") {
-                it.description = "My cookie"
-            }
-            .header<String>("x-my-header") {
-                it.description = "My header"
-            }
-            .pathParam<Int>("my-path-param") {
-                it.description = "My path param"
-            }
-            .queryParam<String>("name") {
-                it.description = "The name of the users you want to filter"
-                it.required = true
-                it.deprecated = true
-                it.allowEmptyValue = true
-            }
-            .queryParam<Int>("age")
-            .jsonArray<User>("200")
+        .operation {
+            it.addTagsItem("user")
+        }
+        .cookie<String>("my-cookie") {
+            it.description = "My cookie"
+        }
+        .header<String>("x-my-header") {
+            it.description = "My header"
+        }
+        .pathParam<Int>("my-path-param") {
+            it.description = "My path param"
+        }
+        .queryParam<String>("name") {
+            it.description = "The name of the users you want to filter"
+            it.required = true
+            it.deprecated = true
+            it.allowEmptyValue = true
+        }
+        .queryParam<Int>("age")
+        .jsonArray<User>("200")
     app.get("/users/:my-path-param", documented(getUsersDocumentation) {
         val myCookie = it.cookie("my-cookie")
         val myHeader = it.cookie("x-my-header")
@@ -165,46 +170,46 @@ fun buildComplexExample(options: OpenApiOptions): Javalin {
     })
 
     val getUsers2Documentation = document()
-            .operation {
-                it.addTagsItem("user")
-                it.security(listOf(
-                        SecurityRequirement().addList("http", listOf("myScope"))
-                ))
-            }
-            .json<Array<User>>("200")
+        .operation {
+            it.addTagsItem("user")
+            it.security(listOf(
+                SecurityRequirement().addList("http", listOf("myScope"))
+            ))
+        }
+        .json<Array<User>>("200")
     app.get("/users2", documented(getUsers2Documentation) { it.json(listOf(User(name = "Jim"))) })
 
     val getFormDataDocumentation = document()
-            .formParam<String>("name", required = true)
-            .formParam<Int>("age")
-            .result<Unit>("200")
+        .formParam<String>("name", required = true)
+        .formParam<Int>("age")
+        .result<Unit>("200")
     app.put("/form-data", documented(getFormDataDocumentation) {
         it.formParam("name")
         it.formParam("age")
     })
 
     val getFormDataSchemaDocumentation = document()
-            .formParamBody<Address>()
-            .result<Unit>("200")
+        .formParamBody<Address>()
+        .result<Unit>("200")
     app.put("/form-data-schema", documented(getFormDataSchemaDocumentation) {})
 
     val getFormDataSchemaMultipartDocumentation = document()
-            .formParamBody<Address>(contentType = ContentType.FORM_DATA_MULTIPART)
-            .result<Unit>("200")
+        .formParamBody<Address>(contentType = ContentType.FORM_DATA_MULTIPART)
+        .result<Unit>("200")
     app.put("/form-data-schema-multipart", documented(getFormDataSchemaMultipartDocumentation) {})
 
     val putUserDocumentation = document()
-            .operation {
-                it.addTagsItem("user")
-            }
-            .body<String> {
-                it.description = "body description"
-                it.required = true
-            }
-            .body<User>("application/json")
-            .body<User>("application/xml")
-            .bodyAsBytes()
-            .bodyAsBytes("image/png")
+        .operation {
+            it.addTagsItem("user")
+        }
+        .body<String> {
+            it.description = "body description"
+            it.required = true
+        }
+        .body<User>("application/json")
+        .body<User>("application/xml")
+        .bodyAsBytes()
+        .bodyAsBytes("image/png")
     app.put("/user", documented(putUserDocumentation) {
         val userString = it.body()
         val user = it.bodyAsClass(User::class.java)
@@ -213,57 +218,57 @@ fun buildComplexExample(options: OpenApiOptions): Javalin {
     })
 
     val getStringDocumentation = OpenApiDocumentation()
-            .result<String>("200")
+        .result<String>("200")
     app.get("/string", documented(getStringDocumentation) {
         it.result("Hello")
     })
 
     val getHomepageDocumentation = OpenApiDocumentation()
-            .html("200") {
-                it.description = "My Homepage"
-            }
+        .html("200") {
+            it.description = "My Homepage"
+        }
     app.get("/homepage", documented(getHomepageDocumentation) {
         it.html("<p>Hello World</p>")
     })
 
     val getUploadDocumentation = OpenApiDocumentation()
-            .uploadedFile("file") {
-                it.description = "MyFile"
-                it.required = true
-            }
+        .uploadedFile("file") {
+            it.description = "MyFile"
+            it.required = true
+        }
     app.get("/upload", documented(getUploadDocumentation) {
         it.uploadedFile("file")
     })
 
     val getUploadsDocumentation = OpenApiDocumentation()
-            .uploadedFiles("files") {
-                it.description = "MyFiles"
-                it.required = true
-            }
+        .uploadedFiles("files") {
+            it.description = "MyFiles"
+            it.required = true
+        }
     app.get("/uploads", documented(getUploadsDocumentation) {
         it.uploadedFiles("files")
     })
 
     val getUploadWithFormDataDocumentation = OpenApiDocumentation()
-            .uploadedFile("file") {
-                it.description = "MyFile"
-                it.required = true
-            }
-            .formParam<String>("title")
+        .uploadedFile("file") {
+            it.description = "MyFile"
+            it.required = true
+        }
+        .formParam<String>("title")
     app.get("/upload-with-form-data", documented(getUploadWithFormDataDocumentation) {
         it.uploadedFile("file")
         it.formParam("title")
     })
 
     val getResourcesDocumentation = OpenApiDocumentation()
-            .result<Unit>("200")
+        .result<Unit>("200")
     app.get("/resources/*", documented(getResourcesDocumentation) {})
 
     app.get("/ignored", documented(document().ignore()) {})
 
     app.get("/enums/:my-enum-path-param", documented(document()
-            .pathParam<UserType>("my-enum-path-param")
-            .queryParam<UserType>("my-enum-query-param")) {})
+        .pathParam<UserType>("my-enum-path-param")
+        .queryParam<UserType>("my-enum-query-param")) {})
 
     return app
 }
@@ -286,8 +291,8 @@ class TestOpenApi {
         }
 
         val userCrudHandlerDocumentation = documentCrud()
-                .getOne(document().json<User>("200"))
-                .getAll(document().jsonArray<User>("200"))
+            .getOne(document().json<User>("200"))
+            .getAll(document().jsonArray<User>("200"))
 
         class UserCrudHandler : CrudHandler {
             override fun getAll(ctx: Context) {
@@ -357,28 +362,28 @@ class TestOpenApi {
         app.put("/user") {}
 
         assertThatExceptionOfType(IllegalStateException::class.java)
-                .isThrownBy {
-                    JavalinOpenApi.createSchema(app)
-                }
-                .withMessage("You need to register the \"OpenApiPlugin\" before you can create the OpenAPI schema")
+            .isThrownBy {
+                JavalinOpenApi.createSchema(app)
+            }
+            .withMessage("You need to register the \"OpenApiPlugin\" before you can create the OpenAPI schema")
     }
 
     @Test
     fun `createSchema works with default operation`() {
         val openApiOptions = OpenApiOptions(
-                Info().title("Example").version("1.0.0")
+            Info().title("Example").version("1.0.0")
         )
-                .defaultDocumentation { documentation ->
-                    documentation.result<MyError>("500") {
-                        it.description = "Server Error"
-                    }
+            .defaultDocumentation { documentation ->
+                documentation.result<MyError>("500") {
+                    it.description = "Server Error"
                 }
+            }
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
         }
 
         val route2Documentation = document()
-                .json<User>("200")
+            .json<User>("200")
 
         with(app) {
             get("/route1", documented(route2Documentation) {})
@@ -393,7 +398,7 @@ class TestOpenApi {
     @Test
     fun `createSchema works with repeatable query param and dsl`() {
         val openApiOptions = OpenApiOptions(
-                Info().title("Example").version("1.0.0")
+            Info().title("Example").version("1.0.0")
         )
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
@@ -410,7 +415,7 @@ class TestOpenApi {
     @Test
     fun `createSchema works with query param and reified dsl`() {
         val openApiOptions = OpenApiOptions(
-                Info().title("Example").version("1.0.0")
+            Info().title("Example").version("1.0.0")
         )
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
@@ -427,7 +432,7 @@ class TestOpenApi {
     @Test
     fun `createSchema works with query param and dsl`() {
         val openApiOptions = OpenApiOptions(
-                Info().title("Example").version("1.0.0")
+            Info().title("Example").version("1.0.0")
         )
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
@@ -444,19 +449,19 @@ class TestOpenApi {
     @Test
     fun `createSchema applies defaults before actual documentation`() {
         val openApiOptions = OpenApiOptions(
-                Info().title("Example").version("1.0.0")
+            Info().title("Example").version("1.0.0")
         )
-                .defaultDocumentation { documentation -> documentation.ignore() }
+            .defaultDocumentation { documentation -> documentation.ignore() }
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
         }
 
         val route1Documentation = document()
-                .ignore(false)
-                .json<User>("200")
+            .ignore(false)
+            .json<User>("200")
 
         val route3Documentation = document()
-                .json<User>("200")
+            .json<User>("200")
 
         with(app) {
             get("/route1", documented(route1Documentation) {})
@@ -476,13 +481,13 @@ class TestOpenApi {
         val customMapper = JacksonToJsonMapper.createObjectMapperWithDefaults()
 
         val openApiOptions = OpenApiOptions(Info().title("Example").version("1.0.0"))
-                .jacksonMapper(customMapper)
+            .jacksonMapper(customMapper)
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
         }
 
         val logsDocumentation = document()
-                .jsonArray<Log>("200")
+            .jsonArray<Log>("200")
 
         with(app) {
             get("/logs", documented(logsDocumentation) {})
@@ -495,20 +500,44 @@ class TestOpenApi {
         assertThat(timestampSchemaType.format).isEqualTo("int64")
     }
 
+    @Test
+    fun `createSchema Instants respect annotations`() {
+        val customMapper = JacksonToJsonMapper.createObjectMapperWithDefaults()
+
+        val openApiOptions = OpenApiOptions(Info().title("Example").version("1.0.0"))
+            .jacksonMapper(customMapper)
+        val app = Javalin.create {
+            it.registerPlugin(OpenApiPlugin(openApiOptions))
+        }
+
+        val logsDocumentation = document()
+            .jsonArray<AnnotatedLog>("200")
+
+        with(app) {
+            get("/logs", documented(logsDocumentation) {})
+        }
+
+        val actual = JavalinOpenApi.createSchema(app)
+        val schema = actual.components.schemas["AnnotatedLog"]!!
+        val timestampSchemaType = schema.properties["timestamp"]!!
+        assertThat(timestampSchemaType.type).isEqualTo("string")
+        assertThat(timestampSchemaType.example).isEqualTo("test example")
+    }
+
     @Test
     fun `createSchema works with Instants as strings`() {
         val customMapper = JacksonToJsonMapper.createObjectMapperWithDefaults()
-                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
+            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
 
         val openApiOptions = OpenApiOptions(
-                Info().title("Example").version("1.0.0")
+            Info().title("Example").version("1.0.0")
         ).jacksonMapper(customMapper)
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(openApiOptions))
         }
 
         val logsDocumentation = document()
-                .jsonArray<Log>("200")
+            .jsonArray<Log>("200")
 
         with(app) {
             get("/logs", documented(logsDocumentation) {})
@@ -525,32 +554,32 @@ class TestOpenApi {
     fun `createSchema works with composed body and response`() {
         val app = Javalin.create {
             it.registerPlugin(OpenApiPlugin(OpenApiOptions(
-                    Info().title("Example").version("1.0.0")
+                Info().title("Example").version("1.0.0")
             )))
         }
 
         val anyOfBodyDocumentation = document()
-                .body(anyOf(documentedContent<Address>(), documentedContent<User>(isArray = true)))
-                .operation {
-                    it.summary = "Get body with any of objects"
-                    it.operationId = "composedBodyAnyOf"
-                }
+            .body(anyOf(documentedContent<Address>(), documentedContent<User>(isArray = true)))
+            .operation {
+                it.summary = "Get body with any of objects"
+                it.operationId = "composedBodyAnyOf"
+            }
         app.get("/composed-body/any-of", documented(anyOfBodyDocumentation) {})
 
         val oneOfBodyDocumentation = document()
-                .body(oneOf(documentedContent<Address>(), documentedContent<User>(isArray = true)))
-                .operation {
-                    it.summary = "Get body with one of objects"
-                    it.operationId = "composedBodyOneOf"
-                }
+            .body(oneOf(documentedContent<Address>(), documentedContent<User>(isArray = true)))
+            .operation {
+                it.summary = "Get body with one of objects"
+                it.operationId = "composedBodyOneOf"
+            }
         app.get("/composed-body/one-of", documented(oneOfBodyDocumentation) {})
 
         val oneOfResponseDocumentation = document()
-                .result("200", oneOf(documentedContent<Address>(), documentedContent<User>(), documentedContent<User>(contentType = "application/xml")))
-                .operation {
-                    it.summary = "Get with one of responses"
-                    it.operationId = "composedResponseOneOf"
-                }
+            .result("200", oneOf(documentedContent<Address>(), documentedContent<User>(), documentedContent<User>(contentType = "application/xml")))
+            .operation {
+                it.summary = "Get with one of responses"
+                it.operationId = "composedResponseOneOf"
+            }
         app.get("/composed-response/one-of", documented(oneOfResponseDocumentation) {})
 
         val actual = JavalinOpenApi.createSchema(app)
@@ -561,10 +590,10 @@ class TestOpenApi {
     fun `enableOpenApi provides get route if path is given`() {
         TestUtil.test(Javalin.create {
             it.registerPlugin(OpenApiPlugin(OpenApiOptions(
-                    Info().apply {
-                        title = "Example"
-                        version = "1.0.0"
-                    }
+                Info().apply {
+                    title = "Example"
+                    version = "1.0.0"
+                }
             ).path("/docs/swagger.json")))
         }) { app, http ->
             app.get("/test") {}
@@ -605,7 +634,7 @@ class TestOpenApi {
                     version = "1.0.0"
                 })
             }.path("/docs/swagger.json")
-                    .responseModifier(modifier)
+                .responseModifier(modifier)
             ))
         }) { app, http ->
             app.get("/test") {}
@@ -633,8 +662,8 @@ class TestOpenApi {
                     version = "1.0.0"
                 })
             }.path("/docs/swagger.json")
-                    .responseModifier(modifier)
-                    .disableCaching()
+                .responseModifier(modifier)
+                .disableCaching()
             ))
         }) { app, http ->
             app.get("/test") {}
@@ -672,14 +701,14 @@ class TestOpenApi {
     fun `setDocumentation works`() {
         val app = Javalin.create {
             it.registerPlugin(
-                    OpenApiPlugin(OpenApiOptions(Info().version("1.0.0").title("Override Example"))
-                            .setDocumentation("/user", HttpMethod.POST, document().operation { operation ->
-                                operation.description = "get description overwritten"
-                            })
-                            .setDocumentation("/user", HttpMethod.GET, document().operation { operation ->
-                                operation.description = "post description overwritten"
-                            }.result<User>("200"))
-                    )
+                OpenApiPlugin(OpenApiOptions(Info().version("1.0.0").title("Override Example"))
+                    .setDocumentation("/user", HttpMethod.POST, document().operation { operation ->
+                        operation.description = "get description overwritten"
+                    })
+                    .setDocumentation("/user", HttpMethod.GET, document().operation { operation ->
+                        operation.description = "post description overwritten"
+                    }.result<User>("200"))
+                )
             )
         }
 
@@ -702,15 +731,15 @@ class TestOpenApi {
     fun `setDocumentation with non existing path works`() {
         val app = Javalin.create {
             it.registerPlugin(
-                    OpenApiPlugin(OpenApiOptions(Info().version("1.0.0").title("Override Example"))
-                            .setDocumentation("/user", HttpMethod.POST, document().operation { operation ->
-                                operation.description = "get description overwritten"
-                            })
-                            .setDocumentation("/user", HttpMethod.GET, document().operation { operation ->
-                                operation.description = "post description overwritten"
-                            }.result<User>("200"))
-                            .setDocumentation("/unimplemented", HttpMethod.GET, document())
-                    )
+                OpenApiPlugin(OpenApiOptions(Info().version("1.0.0").title("Override Example"))
+                    .setDocumentation("/user", HttpMethod.POST, document().operation { operation ->
+                        operation.description = "get description overwritten"
+                    })
+                    .setDocumentation("/user", HttpMethod.GET, document().operation { operation ->
+                        operation.description = "post description overwritten"
+                    }.result<User>("200"))
+                    .setDocumentation("/unimplemented", HttpMethod.GET, document())
+                )
             )
         }
 
@@ -762,7 +791,7 @@ class TestOpenApi {
     @Test
     fun `ignorePath with http methods works`() {
         val app = buildComplexExample(OpenApiOptions(::createComplexExampleBaseConfiguration)
-                .ignorePath("/user", HttpMethod.PUT))
+            .ignorePath("/user", HttpMethod.PUT))
         val actual = JavalinOpenApi.createSchema(app)
         val json = actual.asJson().getJSONObject("paths").getJSONObject("/user").toString()
 
@@ -772,7 +801,7 @@ class TestOpenApi {
     @Test
     fun `ignorePath with prefix works`() {
         val app = buildComplexExample(OpenApiOptions(::createComplexExampleBaseConfiguration)
-                .ignorePath("/user*"))
+            .ignorePath("/user*"))
         val actual = JavalinOpenApi.createSchema(app)
         val json = actual.asJson().getJSONObject("paths")
         val userJson = json.getJSONObject("/user").toString()
@@ -784,7 +813,7 @@ class TestOpenApi {
     @Test
     fun `ignorePath with prefix and http methods works`() {
         val app = buildComplexExample(OpenApiOptions(::createComplexExampleBaseConfiguration)
-                .ignorePath("/user*", HttpMethod.PUT))
+            .ignorePath("/user*", HttpMethod.PUT))
         val actual = JavalinOpenApi.createSchema(app)
         val json = actual.asJson().getJSONObject("paths")
         val userJson = json.getJSONObject("/user").toString()
diff --git a/javalin/src/test/java/io/javalin/TestFuture.kt b/javalin/src/test/java/io/javalin/TestFuture.kt
index b94095f3..eac994f8 100644
--- a/javalin/src/test/java/io/javalin/TestFuture.kt
+++ b/javalin/src/test/java/io/javalin/TestFuture.kt
@@ -1,12 +1,8 @@
 package io.javalin
 
-import com.mashape.unirest.http.exceptions.UnirestException
 import io.javalin.core.util.Header
-import io.javalin.http.context.queryParam
-import io.javalin.testing.SerializeableObject
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
-import org.assertj.core.api.Assertions.fail
 import org.junit.Test
 import java.io.IOException
 import java.io.InputStream
@@ -19,61 +15,58 @@ class TestFuture {
 
     @Test
     fun `hello future world`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture("Result")) }
+        app.get("/test-future") { it.future(getFuture("Result")) }
         assertThat(http.getBody("/test-future")).isEqualTo("Result")
     }
 
     @Test
     fun `after-handlers run after future is resolved`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture("Not result")) }
+        app.get("/test-future") { it.future(getFuture("Not result")) }
         app.after { ctx -> ctx.result("Overwritten by after-handler") }
         assertThat(http.getBody("/test-future")).isEqualTo("Overwritten by after-handler")
     }
 
-    @Test
-    fun `setting future in after-handler throws`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture("Not result")) }
-        app.after("/test-future") { ctx -> ctx.result(getFuture("Overwritten by after-handler")) }
-        assertThat(http.getBody("/test-future")).isEqualTo("Internal server error")
-    }
-
     @Test
     fun `error-handlers run after future is resolved`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture("Not result")).status(555) }
+        app.get("/test-future") { it.status(555).future(getFuture("Not result")) }
         app.error(555) { ctx -> ctx.result("Overwritten by error-handler") }
         assertThat(http.getBody("/test-future")).isEqualTo("Overwritten by error-handler")
     }
 
     @Test
     fun `unresolved future throws`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture(null)) }
-        assertThat(http.getBody("/test-future")).isEqualTo("Internal server error")
-    }
-
-    @Test
-    fun `future throws`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture(null)) }
+        app.get("/test-future") { it.future(getFuture(null)) }
         assertThat(http.getBody("/test-future")).isEqualTo("Internal server error")
     }
 
     @Test
     fun `unresolved futures are handled by exception-mapper`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFuture(null)) }
+        app.get("/test-future") { it.future(getFuture(null)) }
         app.exception(CancellationException::class.java) { _, ctx -> ctx.result("Handled") }
         assertThat(http.getBody("/test-future")).isEqualTo("Handled")
     }
 
     @Test
     fun `futures failures are handled by exception-mapper`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.json(getFailingFuture(UnsupportedOperationException())) }
+        app.get("/test-future") { ctx ->
+            ctx.future(getFailingFuture(UnsupportedOperationException()))
+        }
         app.exception(UnsupportedOperationException::class.java) { _, ctx -> ctx.result("Handled") }
         assertThat(http.getBody("/test-future")).isEqualTo("Handled")
     }
 
     @Test
-    fun `setting a future in an exception-handler throws`() = TestUtil.test { app, http ->
-        app.get("/test-future") { throw Exception() }
-        app.exception(Exception::class.java) { _, ctx -> ctx.result(getFuture("Exception result")) }
+    fun `calling future in after-handler throws`() = TestUtil.test { app, http ->
+        app.get("/test-future") { it.future(getFuture("Not result")) }
+        app.after("/test-future") { ctx -> ctx.future(getFuture("Not result")) }
+        assertThat(http.get("/test-future").body).isEqualTo("Internal server error")
+        assertThat(http.get("/test-future").status).isEqualTo(500)
+    }
+
+    @Test
+    fun `calling future in an exception-handler throws`() = TestUtil.test { app, http ->
+        app.get("/test-future") { ctx -> throw Exception() }
+        app.exception(Exception::class.java) { _, ctx -> ctx.future(getFuture("Not result")) }
         assertThat(http.getBody("/test-future")).isEqualTo("")
         assertThat(http.get("/test-future").status).isEqualTo(500)
     }
@@ -81,40 +74,35 @@ class TestFuture {
     @Test
     fun `future is overwritten if String result is set`() = TestUtil.test { app, http ->
         app.get("/test-future") { ctx ->
-            ctx.result(getFuture("Result"))
+            ctx.future(getFuture("Result"))
             ctx.result("Overridden")
         }
         assertThat(http.getBody("/test-future")).isEqualTo("Overridden")
     }
 
     @Test
-    fun `future throws when stream throws`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> ctx.result(getFutureFailingStream()) }
-        try {
-            assertThat(http.get("/test-future").status).isEqualTo(500)
-        } catch (e: UnirestException) { // We need to catch Unirest's exception, as TestUtil swallows it
-            fail("Unirest is not supposed to throw", e)
-        }
+    fun `exceptions that occur during response writing are handled`() = TestUtil.test { app, http ->
+        app.get("/test-future") { it.future(getFutureFailingStream()) }
+        assertThat(http.get("/test-future").body).isEqualTo("")
+        assertThat(http.get("/test-future").status).isEqualTo(500)
     }
 
     @Test
     fun `loonyrunes is happy with the api`() = TestUtil.test { app, http ->
         app.get("/") { ctx ->
-            val noContent = ctx.queryParam("no-content") != null
-            ctx.status(404) // should never happen
-            ctx.json(CompletableFuture.supplyAsync {
-                if (noContent) {
-                    ctx.status(204)
-                    null
-                } else {
+            val hasContent = ctx.queryParam("has-content") != null
+            ctx.future(getFuture("some-future-result")) { result ->
+                if (hasContent) {
                     ctx.status(200)
-                    SerializeableObject()
+                    ctx.json(result!!)
+                } else {
+                    ctx.status(204)
                 }
-            })
+            }
         }
-        val contentResponse = http.get("/")
+        val contentResponse = http.get("/?has-content")
         assertThat(contentResponse.status).isEqualTo(200)
-        assertThat(contentResponse.body).isEqualTo("""{"value1":"FirstValue","value2":"SecondValue"}""")
+        assertThat(contentResponse.body).isEqualTo("""some-future-result""")
         assertThat(contentResponse.headers.getFirst(Header.CONTENT_TYPE)).isEqualTo("application/json")
         val noContentResponse = http.get("/?no-content")
         assertThat(noContentResponse.status).isEqualTo(204)
diff --git a/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt b/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt
index af755731..d21b77b7 100644
--- a/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt
+++ b/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt
@@ -50,12 +50,13 @@ class TestHttpResponseExceptions {
         val response = http.post("/").header(Header.ACCEPT, "application/json").asString()
         assertThat(response.headers.getFirst(Header.CONTENT_TYPE)).isEqualTo("application/json")
         assertThat(response.status).isEqualTo(HttpStatus.FORBIDDEN_403)
-        assertThat(response.body).isEqualTo("""{
-                |    "title": "Off limits!",
-                |    "status": 403,
-                |    "type": "https://javalin.io/documentation#forbiddenresponse",
-                |    "details": {}
-                |}""".trimMargin()
+        assertThat(response.body).isEqualTo(
+            """{
+            |    "title": "Off limits!",
+            |    "status": 403,
+            |    "type": "https://javalin.io/documentation#forbiddenresponse",
+            |    "details": {}
+            |}""".trimMargin()
         )
     }
 
@@ -66,7 +67,8 @@ class TestHttpResponseExceptions {
         app.post("/") { throw CustomResponse() }
         val response = http.post("/").header(Header.ACCEPT, "application/json").asString()
         assertThat(response.status).isEqualTo(418)
-        assertThat(response.body).isEqualTo("""{
+        assertThat(response.body).isEqualTo(
+            """{
                 |    "title": "",
                 |    "status": 418,
                 |    "type": "https://javalin.io/documentation#error-responses",
@@ -107,7 +109,7 @@ class TestHttpResponseExceptions {
             }, 0, TimeUnit.MILLISECONDS)
             return future
         }
-        app.get("/completed-future-route") { ctx -> ctx.result(getExceptionallyCompletingFuture()) }
+        app.get("/completed-future-route") { it.future(getExceptionallyCompletingFuture()) }
         assertThat(http.get("/completed-future-route").body).isEqualTo("Unauthorized")
         assertThat(http.get("/completed-future-route").status).isEqualTo(401)
     }
@@ -120,7 +122,7 @@ class TestHttpResponseExceptions {
             }
             "Result"
         }
-        app.get("/throwing-future-route") { ctx -> ctx.result(getThrowingFuture()) }
+        app.get("/throwing-future-route") { it.future(getThrowingFuture()) }
         assertThat(http.get("/throwing-future-route").body).isEqualTo("Unauthorized")
         assertThat(http.get("/throwing-future-route").status).isEqualTo(401)
     }
@@ -134,7 +136,7 @@ class TestHttpResponseExceptions {
             }, 0, TimeUnit.MILLISECONDS)
             return future
         }
-        app.get("/completed-future-route") { ctx -> ctx.result(getUnexpectedExceptionallyCompletingFuture()) }
+        app.get("/completed-future-route") { it.future(getUnexpectedExceptionallyCompletingFuture()) }
         app.exception(IllegalStateException::class.java) { exception, ctx -> ctx.result(exception.message!!) }
         assertThat(http.get("/completed-future-route").body).isEqualTo("Unexpected message")
     }
@@ -145,12 +147,13 @@ class TestHttpResponseExceptions {
         val response = http.get("/content-type")
         assertThat(response.status).isEqualTo(HttpStatus.FORBIDDEN_403)
         assertThat(response.headers.getFirst(Header.CONTENT_TYPE)).isEqualTo("application/json")
-        assertThat(response.body).isEqualTo("""{
-                |    "title": "Forbidden",
-                |    "status": 403,
-                |    "type": "https://javalin.io/documentation#forbiddenresponse",
-                |    "details": {}
-                |}""".trimMargin()
+        assertThat(response.body).isEqualTo(
+            """{
+            |    "title": "Forbidden",
+            |    "status": 403,
+            |    "type": "https://javalin.io/documentation#forbiddenresponse",
+            |    "details": {}
+            |}""".trimMargin()
         )
     }
 
@@ -158,12 +161,13 @@ class TestHttpResponseExceptions {
     fun `default exceptions work well with custom content-typed errors`() = TestUtil.test { app, http ->
         app.get("/") { throw ForbiddenResponse("Off limits!") }
         app.error(403, "html") { it.result("Only mapped for HTML") }
-        assertThat(http.jsonGet("/").body).isEqualTo("""{
-                |    "title": "Off limits!",
-                |    "status": 403,
-                |    "type": "https://javalin.io/documentation#forbiddenresponse",
-                |    "details": {}
-                |}""".trimMargin()
+        assertThat(http.jsonGet("/").body).isEqualTo(
+            """{
+            |    "title": "Off limits!",
+            |    "status": 403,
+            |    "type": "https://javalin.io/documentation#forbiddenresponse",
+            |    "details": {}
+            |}""".trimMargin()
         )
         assertThat(http.htmlGet("/").body).isEqualTo("Only mapped for HTML")
     }
@@ -179,12 +183,13 @@ class TestHttpResponseExceptions {
     @Test
     fun `details are displayed as a map in json`() = TestUtil.test { app, http ->
         app.get("/") { throw ForbiddenResponse("Off limits!", mapOf("a" to "A", "b" to "B")) }
-        assertThat(http.jsonGet("/").body).isEqualTo("""{
-                |    "title": "Off limits!",
-                |    "status": 403,
-                |    "type": "https://javalin.io/documentation#forbiddenresponse",
-                |    "details": {"a":"A","b":"B"}
-                |}""".trimMargin()
+        assertThat(http.jsonGet("/").body).isEqualTo(
+            """{
+            |    "title": "Off limits!",
+            |    "status": 403,
+            |    "type": "https://javalin.io/documentation#forbiddenresponse",
+            |    "details": {"a":"A","b":"B"}
+            |}""".trimMargin()
         )
     }
 
diff --git a/javalin/src/test/java/io/javalin/TestLogging.kt b/javalin/src/test/java/io/javalin/TestLogging.kt
index 5e2b0b5f..2d8270b4 100644
--- a/javalin/src/test/java/io/javalin/TestLogging.kt
+++ b/javalin/src/test/java/io/javalin/TestLogging.kt
@@ -47,7 +47,7 @@ class TestLogging {
         app.get("/async") { ctx ->
             val future = CompletableFuture<String>()
             Executors.newSingleThreadScheduledExecutor().schedule<Boolean>({ future.complete("Hello Async World!") }, 10, TimeUnit.MILLISECONDS)
-            ctx.result(future)
+            ctx.future(future)
         }
         app.start(0)
         assertThat(HttpUtil(app.port()).getBody("/async")).isEqualTo("Hello Async World!")
diff --git a/javalin/src/test/java/io/javalin/examples/HelloWorldAsync.java b/javalin/src/test/java/io/javalin/examples/HelloWorldAsync.java
index f468756a..1b8c80ff 100644
--- a/javalin/src/test/java/io/javalin/examples/HelloWorldAsync.java
+++ b/javalin/src/test/java/io/javalin/examples/HelloWorldAsync.java
@@ -17,7 +17,7 @@ public class HelloWorldAsync {
         app.get("/", ctx -> {
             CompletableFuture<String> future = new CompletableFuture<>();
             Executors.newSingleThreadScheduledExecutor().schedule(() -> future.complete("Hello World!"), 10, TimeUnit.MILLISECONDS);
-            ctx.result(future);
+            ctx.future(future);
         });
     }
 }
diff --git a/javalin/src/test/java/io/javalin/performance/SimpleAsyncTest.java b/javalin/src/test/java/io/javalin/performance/SimpleAsyncTest.java
index 602e938d..a6d1ea06 100644
--- a/javalin/src/test/java/io/javalin/performance/SimpleAsyncTest.java
+++ b/javalin/src/test/java/io/javalin/performance/SimpleAsyncTest.java
@@ -36,7 +36,7 @@ public class SimpleAsyncTest {
 
         HttpUtil http = new HttpUtil(app.port());
 
-        app.get("/test-async", ctx -> ctx.result(getFuture()));
+        app.get("/test-async", ctx -> ctx.future(getFuture()));
         app.get("/test-sync", ctx -> ctx.result(getBlockingResult()));
 
         timeCallable("Async result", () -> {
diff --git a/javalin/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt b/javalin/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt
index d52ca4d8..d655e9f6 100644
--- a/javalin/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt
+++ b/javalin/src/test/kotlin/io/javalin/examples/HelloWorldAsync.kt
@@ -21,7 +21,7 @@ fun main() {
             val future = CompletableFuture<String>()
             Executors.newSingleThreadScheduledExecutor()
                     .schedule({ future.complete("Hello World!") }, 10, TimeUnit.MILLISECONDS)
-            ctx.result(future)
+            ctx.future(future)
         }
         get("/json") { ctx ->
             val future = CompletableFuture<List<String>>()