Skip to content

Commit 07418d7

Browse files
committed
Merge pull request cdapio#17 from caskdata/feature/exception-handler
NETTY-4: Exception handler
2 parents 13f0724 + 67f382f commit 07418d7

File tree

9 files changed

+149
-63
lines changed

9 files changed

+149
-63
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright © 2015 Cask Data, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package co.cask.http;
18+
19+
import org.jboss.netty.handler.codec.http.HttpRequest;
20+
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
21+
22+
/**
23+
* Handles exceptions and provides a response via the {@link HttpResponder}.
24+
*/
25+
public class ExceptionHandler {
26+
public void handle(Throwable t, HttpRequest request, HttpResponder responder) {
27+
String message = String.format("Exception encountered while processing request : %s", t.getMessage());
28+
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, message);
29+
}
30+
}

src/main/java/co/cask/http/HttpMethodInfo.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,28 @@ class HttpMethodInfo {
3939
private final HttpHandler handler;
4040
private final boolean isChunkedRequest;
4141
private final ChannelBuffer requestContent;
42+
private final HttpRequest request;
4243
private final HttpResponder responder;
4344
private final Object[] args;
4445
private final boolean isStreaming;
46+
private final ExceptionHandler exceptionHandler;
4547

4648
private BodyConsumer bodyConsumer;
4749

48-
HttpMethodInfo(Method method, HttpHandler handler, HttpRequest request, HttpResponder responder, Object[] args) {
50+
HttpMethodInfo(Method method, HttpHandler handler, HttpRequest request, HttpResponder responder, Object[] args,
51+
ExceptionHandler exceptionHandler) {
4952
this.method = method;
5053
this.handler = handler;
5154
this.isChunkedRequest = request.isChunked();
5255
this.requestContent = request.getContent();
53-
this.responder = responder;
5456
this.isStreaming = BodyConsumer.class.isAssignableFrom(method.getReturnType());
57+
this.request = rewriteRequest(request, isStreaming);
58+
this.responder = responder;
59+
this.exceptionHandler = exceptionHandler;
5560

5661
// The actual arguments list to invoke handler method
5762
this.args = new Object[args.length + 2];
58-
this.args[0] = rewriteRequest(request, isStreaming);
63+
this.args[0] = request;
5964
this.args[1] = responder;
6065
System.arraycopy(args, 0, this.args, 2, args.length);
6166
}
@@ -76,7 +81,11 @@ void invoke() throws Exception {
7681
} else {
7782
// Actually <T> would be void
7883
bodyConsumer = null;
79-
method.invoke(handler, args);
84+
try {
85+
method.invoke(handler, args);
86+
} catch (InvocationTargetException e) {
87+
exceptionHandler.handle(e.getTargetException(), request, responder);
88+
}
8089
}
8190
}
8291

src/main/java/co/cask/http/HttpResourceHandler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ public final class HttpResourceHandler implements HttpHandler {
6060
* @param handlers Iterable of HttpHandler.
6161
* @param handlerHooks Iterable of HandlerHook.
6262
* @param urlRewriter URL re-writer.
63+
* @param exceptionHandler Exception handler
6364
*/
6465
public HttpResourceHandler(Iterable<? extends HttpHandler> handlers, Iterable<? extends HandlerHook> handlerHooks,
65-
URLRewriter urlRewriter) {
66+
URLRewriter urlRewriter, ExceptionHandler exceptionHandler) {
6667
//Store the handlers to call init and destroy on all handlers.
6768
this.handlers = ImmutableList.copyOf(handlers);
6869
this.handlerHooks = ImmutableList.copyOf(handlerHooks);
@@ -88,7 +89,8 @@ public HttpResourceHandler(Iterable<? extends HttpHandler> handlers, Iterable<?
8889
Set<HttpMethod> httpMethods = getHttpMethods(method);
8990
Preconditions.checkArgument(httpMethods.size() >= 1,
9091
String.format("No HttpMethod found for method: %s", method.getName()));
91-
patternRouter.add(absolutePath, new HttpResourceModel(httpMethods, absolutePath, method, handler));
92+
patternRouter.add(absolutePath, new HttpResourceModel(httpMethods, absolutePath, method,
93+
handler, exceptionHandler));
9294
} else {
9395
LOG.trace("Not adding method {}({}) to path routing like. HTTP calls will not be routed to this method",
9496
method.getName(), method.getParameterTypes());

src/main/java/co/cask/http/HttpResourceModel.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public final class HttpResourceModel {
5555
private final Method method;
5656
private final HttpHandler handler;
5757
private final List<Map<Class<? extends Annotation>, ParameterInfo<?>>> paramsInfo;
58+
private final ExceptionHandler exceptionHandler;
5859

5960
/**
6061
* Construct a resource model with HttpMethod, method that handles httprequest, Object that contains the method.
@@ -64,12 +65,14 @@ public final class HttpResourceModel {
6465
* @param method handler that handles the http request.
6566
* @param handler instance {@code HttpHandler}.
6667
*/
67-
public HttpResourceModel(Set<HttpMethod> httpMethods, String path, Method method, HttpHandler handler) {
68+
public HttpResourceModel(Set<HttpMethod> httpMethods, String path, Method method, HttpHandler handler,
69+
ExceptionHandler exceptionHandler) {
6870
this.httpMethods = httpMethods;
6971
this.path = path;
7072
this.method = method;
7173
this.handler = handler;
7274
this.paramsInfo = createParametersInfos(method);
75+
this.exceptionHandler = exceptionHandler;
7376
}
7477

7578
/**
@@ -131,7 +134,7 @@ public HttpMethodInfo handle(HttpRequest request, HttpResponder responder, Map<S
131134
idx++;
132135
}
133136

134-
return new HttpMethodInfo(method, handler, request, responder, args);
137+
return new HttpMethodInfo(method, handler, request, responder, args, exceptionHandler);
135138
} else {
136139
//Found a matching resource but could not find the right HttpMethod so return 405
137140
throw new HandlerException(HttpResponseStatus.METHOD_NOT_ALLOWED, String.format

src/main/java/co/cask/http/NettyHttpService.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package co.cask.http;
1818

1919
import com.google.common.base.Function;
20+
import com.google.common.base.Preconditions;
2021
import com.google.common.collect.ImmutableList;
2122
import com.google.common.collect.ImmutableMap;
2223
import com.google.common.collect.Maps;
@@ -42,7 +43,6 @@
4243
import org.slf4j.Logger;
4344
import org.slf4j.LoggerFactory;
4445

45-
import java.io.File;
4646
import java.net.InetSocketAddress;
4747
import java.util.Map;
4848
import java.util.concurrent.Executor;
@@ -93,6 +93,7 @@ public final class NettyHttpService extends AbstractIdleService {
9393
* @param urlRewriter URLRewriter to rewrite incoming URLs.
9494
* @param httpHandlers HttpHandlers to handle the calls.
9595
* @param handlerHooks Hooks to be called before/after request processing by httpHandlers.
96+
*
9697
* @deprecated Use {@link NettyHttpService.Builder} instead.
9798
*/
9899
@Deprecated
@@ -102,19 +103,9 @@ public NettyHttpService(InetSocketAddress bindAddress, int bossThreadPoolSize, i
102103
RejectedExecutionHandler rejectedExecutionHandler, URLRewriter urlRewriter,
103104
Iterable<? extends HttpHandler> httpHandlers,
104105
Iterable<? extends HandlerHook> handlerHooks, int httpChunkLimit) {
105-
this.bindAddress = bindAddress;
106-
this.bossThreadPoolSize = bossThreadPoolSize;
107-
this.workerThreadPoolSize = workerThreadPoolSize;
108-
this.execThreadPoolSize = execThreadPoolSize;
109-
this.execThreadKeepAliveSecs = execThreadKeepAliveSecs;
110-
this.channelConfigs = ImmutableMap.copyOf(channelConfigs);
111-
this.rejectedExecutionHandler = rejectedExecutionHandler;
112-
this.channelGroup = new DefaultChannelGroup();
113-
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter);
114-
this.handlerContext = new BasicHandlerContext(this.resourceHandler);
115-
this.httpChunkLimit = httpChunkLimit;
116-
this.pipelineModifier = null;
117-
this.sslHandlerFactory = null;
106+
this(bindAddress, bossThreadPoolSize, workerThreadPoolSize, execThreadPoolSize, execThreadKeepAliveSecs,
107+
channelConfigs, rejectedExecutionHandler, urlRewriter, httpHandlers, handlerHooks, httpChunkLimit,
108+
null, null, new ExceptionHandler());
118109
}
119110

120111
/**
@@ -131,6 +122,7 @@ public NettyHttpService(InetSocketAddress bindAddress, int bossThreadPoolSize, i
131122
* @param handlerHooks Hooks to be called before/after request processing by httpHandlers.
132123
* @param pipelineModifier Function used to modify the pipeline.
133124
* @param sslHandlerFactory Object used to share SSL certificate details
125+
* @param exceptionHandler Handles exceptions from calling handler methods
134126
*/
135127
private NettyHttpService(InetSocketAddress bindAddress, int bossThreadPoolSize, int workerThreadPoolSize,
136128
int execThreadPoolSize, long execThreadKeepAliveSecs,
@@ -139,7 +131,7 @@ private NettyHttpService(InetSocketAddress bindAddress, int bossThreadPoolSize,
139131
Iterable<? extends HttpHandler> httpHandlers,
140132
Iterable<? extends HandlerHook> handlerHooks, int httpChunkLimit,
141133
Function<ChannelPipeline, ChannelPipeline> pipelineModifier,
142-
SSLHandlerFactory sslHandlerFactory) {
134+
SSLHandlerFactory sslHandlerFactory, ExceptionHandler exceptionHandler) {
143135
this.bindAddress = bindAddress;
144136
this.bossThreadPoolSize = bossThreadPoolSize;
145137
this.workerThreadPoolSize = workerThreadPoolSize;
@@ -148,7 +140,7 @@ private NettyHttpService(InetSocketAddress bindAddress, int bossThreadPoolSize,
148140
this.channelConfigs = ImmutableMap.copyOf(channelConfigs);
149141
this.rejectedExecutionHandler = rejectedExecutionHandler;
150142
this.channelGroup = new DefaultChannelGroup();
151-
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter);
143+
this.resourceHandler = new HttpResourceHandler(httpHandlers, handlerHooks, urlRewriter, exceptionHandler);
152144
this.handlerContext = new BasicHandlerContext(this.resourceHandler);
153145
this.httpChunkLimit = httpChunkLimit;
154146
this.pipelineModifier = pipelineModifier;
@@ -322,6 +314,7 @@ public static class Builder {
322314
private int httpChunkLimit;
323315
private SSLHandlerFactory sslHandlerFactory;
324316
private Function<ChannelPipeline, ChannelPipeline> pipelineModifier;
317+
private ExceptionHandler exceptionHandler;
325318

326319
// Protected constructor to prevent instantiating Builder instance directly.
327320
protected Builder() {
@@ -335,6 +328,7 @@ protected Builder() {
335328
channelConfigs = Maps.newHashMap();
336329
channelConfigs.put("backlog", DEFAULT_CONNECTION_BACKLOG);
337330
sslHandlerFactory = null;
331+
exceptionHandler = new ExceptionHandler();
338332
}
339333

340334
/**
@@ -501,6 +495,12 @@ public Builder enableSSL(SSLConfig sslConfig) {
501495
return this;
502496
}
503497

498+
public Builder setExceptionHandler(ExceptionHandler exceptionHandler) {
499+
Preconditions.checkNotNull(exceptionHandler, "exceptionHandler cannot be null");
500+
this.exceptionHandler = exceptionHandler;
501+
return this;
502+
}
503+
504504
/**
505505
* @return instance of {@code NettyHttpService}
506506
*/
@@ -515,7 +515,7 @@ public NettyHttpService build() {
515515
return new NettyHttpService(bindAddress, bossThreadPoolSize, workerThreadPoolSize,
516516
execThreadPoolSize, execThreadKeepAliveSecs, channelConfigs, rejectedExecutionHandler,
517517
urlRewriter, handlers, handlerHooks, httpChunkLimit, pipelineModifier,
518-
sslHandlerFactory);
518+
sslHandlerFactory, exceptionHandler);
519519
}
520520
}
521521
}

src/test/java/co/cask/http/HttpServerTest.java

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@
3131
import org.jboss.netty.channel.ChannelPipeline;
3232
import org.jboss.netty.handler.codec.http.HttpHeaders;
3333
import org.jboss.netty.handler.codec.http.HttpMethod;
34+
import org.jboss.netty.handler.codec.http.HttpRequest;
3435
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
3536
import org.junit.AfterClass;
3637
import org.junit.Assert;
3738
import org.junit.BeforeClass;
3839
import org.junit.ClassRule;
3940
import org.junit.Test;
4041
import org.junit.rules.TemporaryFolder;
42+
import org.slf4j.Logger;
43+
import org.slf4j.LoggerFactory;
4144

4245
import java.io.File;
4346
import java.io.IOException;
@@ -57,23 +60,32 @@
5760
*/
5861
public class HttpServerTest {
5962

63+
private static final Logger LOG = LoggerFactory.getLogger(HttpServerTest.class);
64+
6065
protected static final Type STRING_MAP_TYPE = new TypeToken<Map<String, String>>() { }.getType();
6166
protected static final Gson GSON = new Gson();
67+
protected static final ExceptionHandler EXCEPTION_HANDLER = new ExceptionHandler() {
68+
@Override
69+
public void handle(Throwable t, HttpRequest request, HttpResponder responder) {
70+
if (t instanceof TestHandler.CustomException) {
71+
responder.sendStatus(TestHandler.CustomException.HTTP_RESPONSE_STATUS);
72+
} else {
73+
super.handle(t, request, responder);
74+
}
75+
}
76+
};
6277

6378
protected static NettyHttpService service;
6479
protected static URI baseURI;
6580

66-
@ClassRule
67-
public static TemporaryFolder tmpFolder = new TemporaryFolder();
68-
69-
@BeforeClass
70-
public static void setup() throws Exception {
81+
protected static NettyHttpService.Builder createBaseNettyHttpServiceBuilder() {
7182
List<HttpHandler> handlers = Lists.newArrayList();
7283
handlers.add(new TestHandler());
7384

7485
NettyHttpService.Builder builder = NettyHttpService.builder();
7586
builder.addHttpHandlers(handlers);
7687
builder.setHttpChunkLimit(75 * 1024);
88+
builder.setExceptionHandler(EXCEPTION_HANDLER);
7789

7890
builder.modifyChannelPipeline(new Function<ChannelPipeline, ChannelPipeline>() {
7991
@Override
@@ -82,8 +94,15 @@ public ChannelPipeline apply(ChannelPipeline channelPipeline) {
8294
return channelPipeline;
8395
}
8496
});
97+
return builder;
98+
}
8599

86-
service = builder.build();
100+
@ClassRule
101+
public static TemporaryFolder tmpFolder = new TemporaryFolder();
102+
103+
@BeforeClass
104+
public static void setup() throws Exception {
105+
service = createBaseNettyHttpServiceBuilder().build();
87106
service.startAndWait();
88107
Service.State state = service.state();
89108
Assert.assertEquals(Service.State.RUNNING, state);
@@ -305,7 +324,7 @@ public void testMultiMatchParamPut() throws Exception {
305324
public void testHandlerException() throws Exception {
306325
HttpURLConnection urlConn = request("/test/v1/uexception", HttpMethod.GET);
307326
Assert.assertEquals(500, urlConn.getResponseCode());
308-
Assert.assertEquals("Exception Encountered while processing request : User Exception",
327+
Assert.assertEquals("Exception encountered while processing request : User Exception",
309328
new String(ByteStreams.toByteArray(urlConn.getErrorStream()), Charsets.UTF_8));
310329
urlConn.disconnect();
311330
}
@@ -474,6 +493,27 @@ public void testUploadReject() throws Exception {
474493
}
475494
}
476495

496+
@Test
497+
public void testSleep() throws Exception {
498+
HttpURLConnection urlConn = request("/test/v1/sleep/10", HttpMethod.GET);
499+
Assert.assertEquals(200, urlConn.getResponseCode());
500+
urlConn.disconnect();
501+
}
502+
503+
@Test
504+
public void testWrongMethod() throws IOException {
505+
HttpURLConnection urlConn = request("/test/v1/customException", HttpMethod.GET);
506+
Assert.assertEquals(HttpResponseStatus.METHOD_NOT_ALLOWED.getCode(), urlConn.getResponseCode());
507+
urlConn.disconnect();
508+
}
509+
510+
@Test
511+
public void testExceptionHandler() throws IOException {
512+
HttpURLConnection urlConn = request("/test/v1/customException", HttpMethod.POST);
513+
Assert.assertEquals(TestHandler.CustomException.HTTP_RESPONSE_STATUS.getCode(), urlConn.getResponseCode());
514+
urlConn.disconnect();
515+
}
516+
477517
protected Socket createRawSocket(URL url) throws IOException {
478518
return new Socket(url.getHost(), url.getPort());
479519
}

0 commit comments

Comments
 (0)