Skip to content

Commit e1d5565

Browse files
committed
jooby-2.2.x - after handle not working with exceptions fix #1405
1 parent 75d7d64 commit e1d5565

File tree

10 files changed

+244
-27
lines changed

10 files changed

+244
-27
lines changed

docs/asciidoc/routing.adoc

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -505,23 +505,25 @@ interface Before {
505505

506506
The javadoc:Route.After[text=after] filter runs after a `handler`.
507507

508-
An `after` filter takes two arguments. The first argument is the `HTTP context`, while the second
509-
argument is the result/response from a **functional handler** or `null` for **side-effects** handler.
508+
An `after` filter takes three arguments. The first argument is the `HTTP context`, the second
509+
argument is the result/response from a **functional handler** or `null` for **side-effects** handler,
510+
the third and last argument is an exception generates from handler.
511+
510512
It expected to operates via side effects, usually modifying the HTTP response (if possible) or
511513
for cleaning/trace execution.
512514

513515
[source,java]
514516
----
515517
interface After {
516-
void apply(Context ctx, Object result);
518+
void apply(Context ctx, Object result, Throwable failure);
517519
}
518520
----
519521

520522
.Functional Handler:
521523
[source,java,role="primary"]
522524
----
523525
{
524-
after((ctx, result) -> {
526+
after((ctx, result, failure) -> {
525527
System.out.println(result); <1>
526528
ctx.setResponseHeader("foo", "bar"); <2>
527529
});
@@ -558,7 +560,7 @@ For **side effects** handler the after filter is invoked with a `null` value and
558560
[source,java,role="primary"]
559561
----
560562
{
561-
after((ctx, result) -> {
563+
after((ctx, result, failure) -> {
562564
System.out.println(result); <1>
563565
ctx.setResponseHeader("foo", "bar"); <2>
564566
});
@@ -597,7 +599,7 @@ You can check whenever you can modify the response by checking the state of java
597599
[source,java,role="primary"]
598600
----
599601
{
600-
after((ctx, result) -> {
602+
after((ctx, result, failure) -> {
601603
if (ctx.isResponseStarted()) {
602604
// Don't modify response
603605
} else {
@@ -621,6 +623,119 @@ You can check whenever you can modify the response by checking the state of java
621623
}
622624
----
623625

626+
[NOTE]
627+
====
628+
An after handler is always invoked.
629+
====
630+
631+
The next examples demonstrate some use cases for dealing with errored responses, but keep in mind
632+
that an after handler is not a mechanism for handling and reporting exceptions that's is a task for an <<error-handler, Error Handler>>.
633+
634+
.Run code depending of success or failure responses:
635+
[source,java,role="primary"]
636+
----
637+
{
638+
after((ctx, result, failure) -> {
639+
if (failure == null) {
640+
db.commit(); <1>
641+
} else {
642+
db.rollback(); <2>
643+
}
644+
});
645+
}
646+
----
647+
648+
.Kotlin
649+
[source,kotlin,role="secondary"]
650+
----
651+
{
652+
after {
653+
if (failure == null) {
654+
db.commit() <1>
655+
} else {
656+
db.rollback() <2>
657+
}
658+
}
659+
}
660+
----
661+
662+
Here the exception is still propagated given the chance to the <<error-handler, Error Handler>> to jump in.
663+
664+
.Recover fom exception and produces an alternative output:
665+
[source,java,role="primary"]
666+
----
667+
{
668+
after((ctx, result, failure) -> {
669+
if (failure instanceOf MyBusinessException) {
670+
ctx.send("Recovering from something"); <1>
671+
}
672+
});
673+
}
674+
----
675+
676+
.Kotlin
677+
[source,kotlin,role="secondary"]
678+
----
679+
{
680+
after {
681+
if (failure is MyBusinessException) {
682+
ctx.send("Recovering from something") <1>
683+
}
684+
}
685+
}
686+
----
687+
688+
<1> Recover and produces an alternative output
689+
690+
Here the exception wont be propagated due we produces a response, so error handler won't be execute it.
691+
692+
In case where the after handler produces a new exception, that exception will be add to the original exception as suppressed exception.
693+
694+
.Suppressed exceptions:
695+
[source,java,role="primary"]
696+
----
697+
{
698+
after((ctx, result, failure) -> {
699+
...
700+
throw new AnotherException();
701+
});
702+
703+
get("/", ctx -> {
704+
...
705+
throw new OriginalException();
706+
});
707+
708+
error((ctx, failure, code) -> {
709+
Throwable originalException = failure; <1>
710+
Throwable anotherException = failure.getSuppressed()[0]; <2>
711+
});
712+
}
713+
----
714+
715+
.Kotlin
716+
[source,kotlin,role="secondary"]
717+
----
718+
{
719+
after {
720+
...
721+
throw AnotherException();
722+
}
723+
724+
get("/") { ctx ->
725+
...
726+
throw OriginalException()
727+
}
728+
729+
error { ctx, failure, code) ->
730+
val originalException = failure <1>
731+
val anotherException = failure.getSuppressed()[0] <2>
732+
}
733+
}
734+
----
735+
736+
<1> Will be `OriginalException`
737+
<2> Will be `AnotherException`
738+
624739
=== Pipeline
625740

626741
Route pipeline (a.k.a route stack) is a composition of one or more decorator(s) tied to a single `handler`:

examples/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@
119119
<artifactId>commons-io</artifactId>
120120
<version>1.3.2</version>
121121
</dependency>
122+
<dependency>
123+
<groupId>org.unbescape</groupId>
124+
<artifactId>unbescape</artifactId>
125+
</dependency>
122126
<dependency>
123127
<groupId>org.jdbi</groupId>
124128
<artifactId>jdbi3-sqlobject</artifactId>

examples/src/main/java/examples/DispatchApp.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ public class DispatchApp extends Jooby {
2020
return next.apply(ctx);
2121
});
2222

23-
after((ctx, value) -> {
24-
});
25-
2623
get("/", ctx -> ctx.query("n").intValue(2));
2724

2825
dispatch(() -> {

examples/src/main/java/examples/HelloApp.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ public class HelloApp extends Jooby {
2222

2323
setRouterOptions(new RouterOptions().setIgnoreCase(false).setIgnoreTrailingSlash(true));
2424

25+
after((ctx, result, failure) -> {
26+
throw new RuntimeException("after");
27+
});
28+
2529
get("/", ctx -> {
26-
return ctx.pathString() + "oo";
30+
throw new IllegalArgumentException("handler");
2731
});
2832

2933
get("/foo/bar", ctx -> {

jooby/src/main/java/io/jooby/Route.java

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ public interface After extends Serializable {
193193
* @return A new filter.
194194
*/
195195
@Nonnull default After then(@Nonnull After next) {
196-
return (ctx, result) -> {
197-
next.apply(ctx, result);
198-
apply(ctx, result);
196+
return (ctx, result, failure) -> {
197+
next.apply(ctx, result, failure);
198+
apply(ctx, result, failure);
199199
};
200200
}
201201

@@ -204,9 +204,11 @@ public interface After extends Serializable {
204204
*
205205
* @param ctx Web context.
206206
* @param result Response generated by route handler.
207+
* @param failure Uncaught exception generated by route handler.
207208
* @throws Exception If something goes wrong.
208209
*/
209-
void apply(@Nonnull Context ctx, @Nullable Object result) throws Exception;
210+
void apply(@Nonnull Context ctx, @Nullable Object result, @Nullable Throwable failure)
211+
throws Exception;
210212
}
211213

212214
/**
@@ -245,14 +247,39 @@ public interface Handler extends Serializable {
245247
*/
246248
@Nonnull default Handler then(@Nonnull After next) {
247249
return ctx -> {
248-
Object result = apply(ctx);
249-
if (ctx.isResponseStarted()) {
250-
Context fwd = Context.readOnly(ctx);
251-
next.apply(fwd, null);
252-
return fwd;
253-
} else {
254-
next.apply(ctx, result);
250+
Throwable cause = null;
251+
Object value = null;
252+
try {
253+
value = apply(ctx);
254+
} catch (Throwable x) {
255+
cause = x;
256+
}
257+
Object result;
258+
try {
259+
if (ctx.isResponseStarted()) {
260+
result = Context.readOnly(ctx);
261+
next.apply((Context) result, value, cause);
262+
} else {
263+
result = value;
264+
next.apply(ctx, value, cause);
265+
}
266+
} catch (Throwable x) {
267+
result = null;
268+
if (cause == null) {
269+
cause = x;
270+
} else {
271+
cause.addSuppressed(x);
272+
}
273+
}
274+
275+
if (cause == null) {
255276
return result;
277+
} else {
278+
if (ctx.isResponseStarted()) {
279+
return ctx;
280+
} else {
281+
throw SneakyThrows.propagate(cause);
282+
}
256283
}
257284
};
258285
}

jooby/src/main/kotlin/io/jooby/HandlerContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
package io.jooby
77

8-
class AfterContext(val ctx: Context, val result: Any?)
8+
class AfterContext(val ctx: Context, val result: Any?, val failure: Any?)
99

1010
class DecoratorContext(val ctx: Context, val next: Route.Handler)
1111

jooby/src/main/kotlin/io/jooby/Kooby.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ open class Kooby constructor() : Jooby() {
134134

135135
@RouterDsl
136136
fun after(handler: AfterContext.() -> Any): Kooby {
137-
super.after { ctx, result -> AfterContext(ctx, result).handler() }
137+
super.after { ctx, result, failure -> AfterContext(ctx, result, failure).handler() }
138138
return this
139139
}
140140

modules/jooby-test/src/test/java/io/jooby/UnitTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void pipeline() {
7777
Jooby app = new Jooby();
7878

7979
app.before(ctx -> ctx.setResponseHeader("before", "<"));
80-
app.after((ctx, result) -> ctx.setResponseHeader("after", ">"));
80+
app.after((ctx, result, failure) -> ctx.setResponseHeader("after", ">"));
8181
app.get("/", ctx -> "OK");
8282

8383
MockRouter router = new MockRouter(app)

tests/src/test/java/io/jooby/FeaturedTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import okhttp3.Response;
2020
import okhttp3.ResponseBody;
2121
import org.junit.jupiter.api.DisplayName;
22-
import org.junit.jupiter.api.RepeatedTest;
2322
import org.junit.jupiter.api.Test;
2423
import reactor.core.publisher.Flux;
2524
import reactor.core.publisher.Mono;
@@ -650,7 +649,7 @@ public void beforeAfter() {
650649
ctx.attribute("buff", buff);
651650
});
652651

653-
app.after((ctx, value) -> {
652+
app.after((ctx, value, failure) -> {
654653
StringBuilder buff = (StringBuilder) value;
655654
buff.append("after1:" + ctx.isInIoThread()).append(";");
656655
});
@@ -661,7 +660,7 @@ public void beforeAfter() {
661660
buff.append("before2:" + ctx.isInIoThread()).append(";");
662661
});
663662

664-
app.after((ctx, value) -> {
663+
app.after((ctx, value, failure) -> {
665664
StringBuilder buff = ctx.attribute("buff");
666665
buff.append("after2:" + ctx.isInIoThread()).append(";");
667666
});

0 commit comments

Comments
 (0)