Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# Vertx Utils changelog
## [2.1.0] - UNRELEASED
### Breaking Changes
* None.
* Updated to use Vert.x v5, which removes deprecated functions from v4, and moves to a `Future` based approach instead of callbacks.

### New Features
* None.

### Enhancements
* None.
* `CATCH_ALL_API_FAILURE_HANDLER` and `CATCH_ALL_API_FAILURE_HANDLER_WITH_EXCEPTION_LOGGING` now:
* Return the status code of a failed `context`, if it is set, instead of always using `500 - INTERNAL_SERVER_ERROR`.
* Capture and ignore `ClosedChannelException` exceptions in addition to `VertxException` versions.

### Fixes
* None.

### Notes
* None.
* Removed the `com.jayway.awaitility:awaitility` dependency. Its scope was `provided`, so this should have no impact.

## [2.0.0] - 2026-05-21
### Breaking Changes
Expand Down
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<parent>
<groupId>com.zepben.maven</groupId>
<artifactId>evolve-super-pom</artifactId>
<version>0.49.0</version>
<version>0.51.0</version>
</parent>

<modelVersion>4.0.0</modelVersion>
Expand Down Expand Up @@ -128,9 +128,9 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.jayway.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>provided</scope>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,31 @@ import io.vertx.core.Handler
import io.vertx.core.VertxException
import io.vertx.ext.web.RoutingContext
import org.slf4j.Logger
import java.nio.channels.ClosedChannelException

object UtilHandlers {

/**
* Returns the default failure handler that if the [RoutingContext.failure] is not null responds with a
* 500 message with our own "standardised" errors JSON response (See [ErrorFormatter.asJson])
* containing the message from the failure. If a logger is specified, it will also log the stacktrace on the server side
* Returns the default failure handler that, if the [RoutingContext.failure] is not null, responds with our own "standardised"
* errors JSON response (See [ErrorFormatter.asJson]). The status of this message will default to 500 if no status is provided,
* and will contain the message from the failure. If a logger is specified, it will also log the stacktrace on the server side.
*/
val CATCH_ALL_API_FAILURE_HANDLER_WITH_EXCEPTION_LOGGING: (Logger?) -> Handler<RoutingContext> = { logger ->
Handler { context: RoutingContext ->
val failure = context.failure()
if (failure != null && !context.response().ended()) {
logger?.error("Error stack trace:", failure)

Respond.withJson(context, HttpResponseStatus.INTERNAL_SERVER_ERROR, ErrorFormatter.asJson(failure.toString()))
// Use the status of the context if it has been set. This allows handlers to simply fail the context (standard Vert.x
// behaviour), instead of needing to send a response to avoid a 500 overwrite of the status.
Respond.withJson(
context,
context.statusCode().takeUnless { it == -1 }?.let { HttpResponseStatus.valueOf(it) }
?: HttpResponseStatus.INTERNAL_SERVER_ERROR,
ErrorFormatter.asJson(failure.toString()),
)
return@Handler
} else if (failure is VertxException && failure.message == "Connection was closed") {
} else if ((failure is VertxException) && (failure.message == "Connection was closed") || (failure is ClosedChannelException)) {
// Don't call context.next() in this case because it logs it. We don't care.
return@Handler
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
*/
package com.zepben.vertxutils.testing

import com.jayway.awaitility.Awaitility
import io.restassured.builder.RequestSpecBuilder
import io.restassured.specification.RequestSpecification
import io.vertx.core.DeploymentOptions
import io.vertx.core.Promise
import io.vertx.core.Vertx
import io.vertx.core.json.JsonObject
import java.io.IOException
import java.lang.AutoCloseable
import java.net.ServerSocket
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

class DeployRestVerticleHelper(
verticleClass: Class<*>,
Expand All @@ -34,32 +31,23 @@ class DeployRestVerticleHelper(
config.put("http.port", port)

// Start the server
val promise = Promise.promise<Void>()
val future = promise.future()
vertx = Vertx.vertx()
val options = DeploymentOptions().setConfig(config)
vertx.deployVerticle(
verticleClass.getName(),
options,
) { ar ->
if (ar!!.succeeded()) promise.complete()
else promise.fail(ar.cause())
runCatching {
vertx.deployVerticle(verticleClass.getName(), DeploymentOptions().setConfig(config))
.await(5, TimeUnit.SECONDS)
}.onFailure {
// Catch any exception raised and convert it to an `AssertionError` for the testing framework.
throw AssertionError(it.message, it)
}

Awaitility.await().atMost(5, TimeUnit.SECONDS).until { future.isComplete }

if (!future.succeeded()) throw AssertionError(future.cause().message)

requestSpec = RequestSpecBuilder().setBaseUri("http://localhost").setPort(port).build()
} catch (ex: IOException) {
throw AssertionError("Failed to start server", ex)
}
}

override fun close() {
val done = AtomicBoolean(false)
vertx.close { done.set(true) }
Awaitility.await().until { done.get() }
vertx.close().await()
}

@get:Throws(IOException::class)
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/zepben/vertxutils/testing/TestHttpServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class TestHttpServer(
server.requestHandler(router)
.listen(
this.randomPortNumber,
) { res ->
).onComplete { res ->
if (res.failed()) throw RuntimeException(res.cause())
latch.countDown()
}
Expand All @@ -55,8 +55,8 @@ class TestHttpServer(

override fun close() {
val latch = CountDownLatch(2)
server.close { latch.countDown() }
vertx.close { latch.countDown() }
server.close().onComplete { latch.countDown() }
vertx.close().onComplete { latch.countDown() }

try {
latch.await()
Expand Down
39 changes: 0 additions & 39 deletions src/test/java/com/zepben/vertxutils/routing/ErrorFormatterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,13 @@
*/
package com.zepben.vertxutils.routing

import com.google.common.net.HttpHeaders
import com.google.common.net.MediaType
import com.zepben.testutils.junit.SystemLogExtension
import com.zepben.vertxutils.routing.ErrorFormatter.asJson
import com.zepben.vertxutils.routing.handlers.UtilHandlers.CATCH_ALL_API_FAILURE_HANDLER
import com.zepben.vertxutils.routing.handlers.UtilHandlers.REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER
import io.vertx.core.http.HttpServerRequest
import io.vertx.core.http.HttpServerResponse
import io.vertx.core.json.JsonObject
import io.vertx.ext.web.RoutingContext
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.mockito.Mockito.*

class ErrorFormatterTest {

Expand All @@ -33,37 +25,6 @@ class ErrorFormatterTest {

}

@Test
fun defaultFailureHandler() {
val context = mock<RoutingContext>()
val response = mock<HttpServerResponse>(RETURNS_SELF)
doReturn(response).`when`(context).response()

val failure: Throwable = RuntimeException("test")
doReturn(failure).`when`(context).failure()
CATCH_ALL_API_FAILURE_HANDLER.handle(context)

verify(response).putHeader(HttpHeaders.CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
verify(response).end(asJson(failure.toString()))
}

@Test
fun redirectNoTrailingToTrailing() {
val context = mock(RoutingContext::class.java)
val request = mock<HttpServerRequest>()
val response = mock<HttpServerResponse>(RETURNS_SELF)
doReturn(request).`when`(context).request()
doReturn(response).`when`(context).response()
doReturn("/some/path/without/slash").`when`(request).path()
doReturn("test=true").`when`(request).query()

REDIRECT_NO_TRAILING_SLASH_TO_TRAILING_SLASH_HANDLER.handle(context)

verify(response).putHeader("Location", "/some/path/without/slash/?test=true")
verify(response).statusCode = 301
verify(response).end()
}

@Test
fun errorToJson() {
val err = "err"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class StaticAssetRoutesTest {
@AfterEach
fun tearDown() {
val latch = CountDownLatch(1)
vertx.close { latch.countDown() }
vertx.close().onComplete { latch.countDown() }
latch.await()
}

Expand Down Expand Up @@ -121,7 +121,7 @@ class StaticAssetRoutesTest {
val router = Router.router(vertx)
vertx.createHttpServer()
.requestHandler(RouteRegister(router, "", true).add(routes).router)
.listen(port) {
.listen(port).onComplete {
latch.countDown()
if (it.failed()) throw RuntimeException("Failed to start server")
}
Expand Down
Loading
Loading