Skip to content

Commit 441e210

Browse files
committed
Treat kotlin.Unit as void in web controllers
This commit fixes a regression introduced by gh-21139 via the usage of Kotlin reflection to invoke HTTP handler methods. It ensures that kotlin.Unit is treated as void by returning null. It also polishes CoroutinesUtils to have a consistent handling compared to the regular case, and adds related tests to prevent future regressions. Closes gh-31648
1 parent c329ed8 commit 441e210

File tree

6 files changed

+79
-4
lines changed

6 files changed

+79
-4
lines changed

spring-core/src/main/java/org/springframework/core/CoroutinesUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public static Publisher<?> invokeSuspendingFunction(CoroutineContext context, Me
123123
}
124124
return KCallables.callSuspendBy(function, argMap, continuation);
125125
})
126-
.filter(result -> !Objects.equals(result, Unit.INSTANCE))
126+
.filter(result -> result != Unit.INSTANCE)
127127
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
128128

129129
KClassifier returnType = function.getReturnType().getClassifier();

spring-core/src/test/kotlin/org/springframework/core/CoroutinesUtilsTests.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import kotlinx.coroutines.*
2020
import kotlinx.coroutines.flow.Flow
2121
import kotlinx.coroutines.flow.flowOf
2222
import kotlinx.coroutines.reactor.awaitSingle
23+
import kotlinx.coroutines.reactor.awaitSingleOrNull
2324
import org.assertj.core.api.Assertions
2425
import org.junit.jupiter.api.Test
2526
import reactor.core.publisher.Flux
@@ -126,6 +127,24 @@ class CoroutinesUtilsTests {
126127
Assertions.assertThatIllegalArgumentException().isThrownBy { CoroutinesUtils.invokeSuspendingFunction(context, method, this, "foo") }
127128
}
128129

130+
@Test
131+
fun invokeSuspendingFunctionReturningUnit() {
132+
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingUnit", Continuation::class.java)
133+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this) as Mono
134+
runBlocking {
135+
Assertions.assertThat(mono.awaitSingleOrNull()).isNull()
136+
}
137+
}
138+
139+
@Test
140+
fun invokeSuspendingFunctionReturningNull() {
141+
val method = CoroutinesUtilsTests::class.java.getDeclaredMethod("suspendingNullable", Continuation::class.java)
142+
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this) as Mono
143+
runBlocking {
144+
Assertions.assertThat(mono.awaitSingleOrNull()).isNull()
145+
}
146+
}
147+
129148
suspend fun suspendingFunction(value: String): String {
130149
delay(1)
131150
return value
@@ -146,4 +165,11 @@ class CoroutinesUtilsTests {
146165
return value
147166
}
148167

168+
suspend fun suspendingUnit() {
169+
}
170+
171+
suspend fun suspendingNullable(): String? {
172+
return null
173+
}
174+
149175
}

spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.Objects;
2424

25+
import kotlin.Unit;
2526
import kotlin.reflect.KFunction;
2627
import kotlin.reflect.KParameter;
2728
import kotlin.reflect.jvm.KCallablesJvm;
@@ -315,7 +316,8 @@ public static Object invokeFunction(Method method, Object target, Object[] args)
315316
}
316317
}
317318
}
318-
return function.callBy(argMap);
319+
Object result = function.callBy(argMap);
320+
return (result == Unit.INSTANCE ? null : result);
319321
}
320322
}
321323

spring-web/src/test/kotlin/org/springframework/web/method/support/InvocableHandlerMethodKotlinTests.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ class InvocableHandlerMethodKotlinTests {
7171
Assertions.assertThat(value).isEqualTo("true")
7272
}
7373

74+
@Test
75+
fun unitReturnValue() {
76+
val value = getInvocable().invokeForRequest(request, null)
77+
Assertions.assertThat(value).isNull()
78+
}
79+
80+
@Test
81+
fun nullReturnValue() {
82+
composite.addResolver(StubArgumentResolver(String::class.java, null))
83+
val value = getInvocable(String::class.java).invokeForRequest(request, null)
84+
Assertions.assertThat(value).isNull()
85+
}
86+
7487
private fun getInvocable(vararg argTypes: Class<*>): InvocableHandlerMethod {
7588
val method = ResolvableMethod.on(Handler::class.java).argTypes(*argTypes).resolveMethod()
7689
val handlerMethod = InvocableHandlerMethod(Handler(), method)
@@ -95,6 +108,14 @@ class InvocableHandlerMethodKotlinTests {
95108

96109
fun nullableBooleanDefaultValue(status: Boolean? = true) =
97110
status.toString()
111+
112+
fun unit(): Unit {
113+
}
114+
115+
@Suppress("UNUSED_PARAMETER")
116+
fun nullable(arg: String?): String? {
117+
return null
118+
}
98119
}
99120

100121
}

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Objects;
2727
import java.util.stream.Stream;
2828

29+
import kotlin.Unit;
2930
import kotlin.coroutines.CoroutineContext;
3031
import kotlin.reflect.KFunction;
3132
import kotlin.reflect.KParameter;
@@ -326,7 +327,8 @@ public static Object invokeFunction(Method method, Object target, Object[] args,
326327
}
327328
}
328329
}
329-
return function.callBy(argMap);
330+
Object result = function.callBy(argMap);
331+
return (result == Unit.INSTANCE ? null : result);
330332
}
331333
}
332334
}

spring-webflux/src/test/kotlin/org/springframework/web/reactive/result/InvocableHandlerMethodKotlinTests.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,20 @@ class InvocableHandlerMethodKotlinTests {
164164
assertHandlerResultValue(result, "override")
165165
}
166166

167+
@Test
168+
fun unitReturnValue() {
169+
val method = NullResultController::unit.javaMethod!!
170+
val result = invoke(NullResultController(), method)
171+
assertHandlerResultValue(result, null)
172+
}
173+
174+
@Test
175+
fun nullReturnValue() {
176+
val method = NullResultController::nullable.javaMethod!!
177+
val result = invoke(NullResultController(), method)
178+
assertHandlerResultValue(result, null)
179+
}
180+
167181

168182
private fun invokeForResult(handler: Any, method: Method, vararg providedArgs: Any): HandlerResult? {
169183
return invoke(handler, method, *providedArgs).block(Duration.ofSeconds(5))
@@ -186,7 +200,7 @@ class InvocableHandlerMethodKotlinTests {
186200
return resolver
187201
}
188202

189-
private fun assertHandlerResultValue(mono: Mono<HandlerResult>, expected: String) {
203+
private fun assertHandlerResultValue(mono: Mono<HandlerResult>, expected: String?) {
190204
StepVerifier.create(mono)
191205
.consumeNextWith {
192206
if (it.returnValue is Mono<*>) {
@@ -242,4 +256,14 @@ class InvocableHandlerMethodKotlinTests {
242256
@Suppress("RedundantSuspendModifier")
243257
suspend fun handleSuspending(@RequestParam value: String = "default") = value
244258
}
259+
260+
class NullResultController {
261+
262+
fun unit() {
263+
}
264+
265+
fun nullable(): String? {
266+
return null
267+
}
268+
}
245269
}

0 commit comments

Comments
 (0)