Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 3f19db1

Browse files
committed
Error handling feature
* plug-able error handler has been add it now * there is a default err handler that logs and send error using the accept header
1 parent f79b168 commit 3f19db1

5 files changed

Lines changed: 146 additions & 53 deletions

File tree

jooby-core/src/main/java/jooby/Jooby.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import javax.annotation.Nonnull;
2121

22+
import jooby.Route.Err;
2223
import jooby.internal.AssetRoute;
2324
import jooby.internal.FallbackBodyConverter;
2425
import jooby.internal.TypeConverters;
@@ -392,6 +393,8 @@ public abstract void configure(@Nonnull Mode mode, @Nonnull Config config,
392393
/** Keep the global injector instance. */
393394
private Injector injector;
394395

396+
private Err err;
397+
395398
{
396399
use(new Jetty());
397400
}
@@ -665,6 +668,20 @@ public Jooby use(final Config config) {
665668
return this;
666669
}
667670

671+
public Jooby err(final Route.Err err) {
672+
this.err = requireNonNull(err, "An err handler is required.");
673+
return this;
674+
}
675+
676+
public Route.Err logError(final Route.Err err) {
677+
requireNonNull(err, "An err handler is required.");
678+
return (req, res, ex) -> {
679+
LoggerFactory.getLogger(Route.Err.class).error("execution of: " + req.path() +
680+
" resulted in exception", ex);
681+
err.handle(req, res, ex);
682+
};
683+
}
684+
668685
/**
669686
* <h1>Bootstrap</h1>
670687
* <p>
@@ -780,6 +797,13 @@ public void configure(final Binder binder) {
780797
converters.addBinding().toInstance(FallbackBodyConverter.COPY_BYTES);
781798
converters.addBinding().toInstance(FallbackBodyConverter.READ_TEXT);
782799
converters.addBinding().toInstance(FallbackBodyConverter.TO_HTML);
800+
801+
// err
802+
if (err == null) {
803+
binder.bind(Err.class).toInstance(logError(new Err.Formatter()));
804+
} else {
805+
binder.bind(Err.class).toInstance(err);
806+
}
783807
}
784808

785809
});

jooby-core/src/main/java/jooby/Route.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import static java.util.Objects.requireNonNull;
44

5+
import java.io.PrintWriter;
6+
import java.io.StringWriter;
57
import java.util.Arrays;
68
import java.util.Collections;
9+
import java.util.LinkedHashMap;
710
import java.util.List;
811
import java.util.Map;
912
import java.util.Optional;
@@ -349,6 +352,47 @@ interface Chain {
349352
void next(Request request, Response response) throws Exception;
350353
}
351354

355+
interface Err {
356+
357+
class Formatter implements Err {
358+
359+
@Override
360+
public void handle(final Request req, final Response res, final Exception ex)
361+
throws Exception {
362+
Map<String, Object> err = err(req, res, ex);
363+
364+
res.format()
365+
.when(MediaType.html, () -> Viewable.of(errPage(req, res, ex), err))
366+
.when(MediaType.all, () -> err)
367+
.send();
368+
}
369+
370+
}
371+
372+
default Map<String, Object> err(final Request req, final Response res, final Exception ex) {
373+
Map<String, Object> error = new LinkedHashMap<>();
374+
HttpStatus status = res.status();
375+
String message = ex.getMessage();
376+
message = message == null ? status.reason() : message;
377+
error.put("message", message);
378+
StringWriter writer = new StringWriter();
379+
ex.printStackTrace(new PrintWriter(writer));
380+
String[] stacktrace = writer.toString().replace("\r", "").split("\\n");
381+
error.put("stacktrace", stacktrace);
382+
error.put("status", res.status().value());
383+
error.put("reason", res.status().reason());
384+
error.put("referer", req.header("referer"));
385+
386+
return error;
387+
}
388+
389+
default String errPage(final Request req, final Response res, final Exception ex) {
390+
return "/err/" + res.status().value();
391+
}
392+
393+
void handle(Request req, Response res, Exception ex) throws Exception;
394+
}
395+
352396
String path();
353397

354398
Request.Verb verb();

jooby-core/src/main/java/jooby/internal/RouteHandler.java

Lines changed: 21 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import static java.util.Objects.requireNonNull;
44

5-
import java.io.PrintWriter;
6-
import java.io.StringWriter;
75
import java.nio.charset.Charset;
86
import java.util.Arrays;
97
import java.util.Collections;
@@ -33,7 +31,7 @@
3331
import jooby.Request;
3432
import jooby.Response;
3533
import jooby.Route;
36-
import jooby.Viewable;
34+
import jooby.Route.Err;
3735

3836
import org.slf4j.Logger;
3937
import org.slf4j.LoggerFactory;
@@ -65,19 +63,23 @@ public class RouteHandler {
6563

6664
private MediaTypeProvider typeProvider;
6765

66+
private Err err;
67+
6868
@Inject
6969
public RouteHandler(final Injector injector,
7070
final BodyConverterSelector selector,
7171
final Set<Request.Module> modules,
7272
final Set<Route.Definition> routes,
7373
final Charset defaultCharset,
74-
final Locale defaultLocale) {
74+
final Locale defaultLocale,
75+
final Route.Err err) {
7576
this.rootInjector = requireNonNull(injector, "An injector is required.");
7677
this.selector = requireNonNull(selector, "A message converter selector is required.");
7778
this.modules = requireNonNull(modules, "Request modules are required.");
7879
this.routeDefs = requireNonNull(routes, "The routes are required.");
7980
this.charset = requireNonNull(defaultCharset, "A defaultCharset is required.");
8081
this.locale = requireNonNull(defaultLocale, "A defaultLocale is required.");
82+
this.err = requireNonNull(err, "An err handler is required.");
8183
this.typeProvider = injector.getInstance(MediaTypeProvider.class);
8284
}
8385

@@ -106,9 +108,9 @@ public void handle(final HttpServletRequest request, final HttpServletResponse r
106108

107109
final String path = verb + requestURI;
108110

109-
log.info("handling: {}", path);
111+
log.debug("handling: {}", path);
110112

111-
log.info(" content-type: {}", type);
113+
log.debug(" content-type: {}", type);
112114

113115
Charset charset = Optional.ofNullable(request.getCharacterEncoding())
114116
.map(Charset::forName)
@@ -142,7 +144,8 @@ public void handle(final HttpServletRequest request, final HttpServletResponse r
142144
.next(reqFactory.apply(injector, notFound), resFactory.apply(injector, notFound));
143145

144146
} catch (Exception ex) {
145-
log.error("handling of: " + path + " ends with error", ex);
147+
log.debug("execution of: " + path + " resulted in exception", ex);
148+
146149
// reset response
147150
response.reset();
148151

@@ -155,20 +158,15 @@ public void handle(final HttpServletRequest request, final HttpServletResponse r
155158
res.header("Cache-Control", NO_CACHE);
156159
res.status(status);
157160

158-
// TODO: move me to an error handler feature
159-
Map<String, Object> model = errorModel(req, ex, status);
160161
try {
161-
res.format()
162-
.when(MediaType.html, () -> Viewable.of("/status/" + status.value(), model))
163-
.when(MediaType.all, () -> model)
164-
.send();
162+
err.handle(req, res, ex);
165163
} catch (Exception ignored) {
166-
log.trace("rendering of error failed, fallback to default error page", ignored);
167-
defaultErrorPage(req, res, status, model);
164+
log.debug("rendering of error failed, fallback to default error page", ignored);
165+
defaultErrorPage(req, res, err.err(req, res, ex));
168166
}
169167
} finally {
170168
long end = System.currentTimeMillis();
171-
log.info(" status -> {} in {}ms", response.getStatus(), end - start);
169+
log.debug(" status -> {} in {}ms", response.getStatus(), end - start);
172170
}
173171
}
174172

@@ -283,8 +281,9 @@ public Request.Verb verb() {
283281
};
284282
}
285283

286-
private void defaultErrorPage(final Request request, final Response response,
287-
final HttpStatus status, final Map<String, Object> model) throws Exception {
284+
private void defaultErrorPage(final Request request, final Response res,
285+
final Map<String, Object> model) throws Exception {
286+
HttpStatus status = res.status();
288287
StringBuilder html = new StringBuilder("<!doctype html>")
289288
.append("<html>\n")
290289
.append("<head>\n")
@@ -308,7 +307,7 @@ private void defaultErrorPage(final Request request, final Response response,
308307

309308
model.remove("reason");
310309

311-
String[] stacktrace = (String[]) model.remove("stackTrace");
310+
String[] stacktrace = (String[]) model.remove("stacktrace");
312311

313312
for (Entry<String, Object> entry : model.entrySet()) {
314313
Object value = entry.getValue();
@@ -319,7 +318,7 @@ private void defaultErrorPage(final Request request, final Response response,
319318
}
320319

321320
if (stacktrace != null) {
322-
html.append("<h2>stackTrace:</h2>\n")
321+
html.append("<h2>stack:</h2>\n")
323322
.append("<div class=\"trace\">\n");
324323

325324
Arrays.stream(stacktrace).forEach(line -> {
@@ -340,45 +339,14 @@ private void defaultErrorPage(final Request request, final Response response,
340339
.append("</body>\n")
341340
.append("</html>\n");
342341

343-
response.header("Cache-Control", NO_CACHE);
344-
response.send(html, FallbackBodyConverter.TO_HTML);
345-
}
346-
347-
private Map<String, Object> errorModel(final Request request, final Exception ex,
348-
final HttpStatus status) {
349-
Map<String, Object> error = new LinkedHashMap<>();
350-
String message = ex.getMessage();
351-
message = message == null ? status.reason() : message;
352-
error.put("message", message);
353-
error.put("stackTrace", dump(ex));
354-
error.put("status", status.value());
355-
error.put("reason", status.reason());
356-
return error;
357-
}
358-
359-
private static String[] dump(final Exception ex) {
360-
StringWriter writer = new StringWriter();
361-
ex.printStackTrace(new PrintWriter(writer));
362-
String[] stacktrace = writer.toString().replace("\r", "").split("\\n");
363-
return stacktrace;
342+
res.header("Cache-Control", NO_CACHE);
343+
res.send(html, FallbackBodyConverter.TO_HTML);
364344
}
365345

366346
private HttpStatus statusCode(final Exception ex) {
367347
if (ex instanceof HttpException) {
368348
return ((HttpException) ex).status();
369349
}
370-
// Class<?> type = ex.getClass();
371-
// Status status = errorMap.get(type);
372-
// while (status == null && type != Throwable.class) {
373-
// type = type.getSuperclass();
374-
// status = errorMap.get(type);
375-
// }
376-
// return status == null ? defaultStatusCode(ex) : status;
377-
// TODO: finished me
378-
return defaultStatusCode(ex);
379-
}
380-
381-
private HttpStatus defaultStatusCode(final Exception ex) {
382350
if (ex instanceof IllegalArgumentException) {
383351
return HttpStatus.BAD_REQUEST;
384352
}

jooby-core/src/main/java/jooby/internal/VariantImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,18 @@ private static BiFunction<String, List<String>, Object> complexConverters(
284284
}
285285
}
286286

287+
288+
@Override
289+
public String toString() {
290+
if (values == null || values.size() == 0) {
291+
return "";
292+
}
293+
if (values.size() == 1) {
294+
return values.get(0);
295+
}
296+
return values.toString();
297+
}
298+
287299
private static Class<?> classFrom(final TypeLiteral<?> type) {
288300
return classFrom(type.getType());
289301
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package jooby;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
import jooby.FilterFeature.HttpResponseValidator;
6+
7+
import org.apache.http.HttpResponse;
8+
import org.apache.http.client.fluent.Request;
9+
import org.apache.http.client.utils.URIBuilder;
10+
import org.apache.http.util.EntityUtils;
11+
import org.junit.Test;
12+
13+
public class ErrHandlerFeature extends ServerFeature {
14+
15+
{
16+
get("/", (req, res) -> {
17+
throw new IllegalArgumentException();
18+
});
19+
20+
err((req, res, ex) -> {
21+
log.error("err", ex);
22+
assertTrue(ex instanceof IllegalArgumentException);
23+
assertEquals(HttpStatus.BAD_REQUEST, res.status());
24+
res.send("err...");
25+
});
26+
}
27+
28+
@Test
29+
public void err() throws Exception {
30+
assertEquals("err...", execute(GET(uri("/")), (response) -> {
31+
assertEquals(400, response.getStatusLine().getStatusCode());
32+
}));
33+
}
34+
35+
private static Request GET(final URIBuilder uri) throws Exception {
36+
return Request.Get(uri.build());
37+
}
38+
39+
private static Object execute(final Request request, final HttpResponseValidator validator)
40+
throws Exception {
41+
HttpResponse resp = request.execute().returnResponse();
42+
validator.validate(resp);
43+
return EntityUtils.toString(resp.getEntity());
44+
}
45+
}

0 commit comments

Comments
 (0)