Permalink
Browse files

DSL for defining custom 404s and Exception handling

  * removes ErrorService
  * moves 404/exception handling to Appservice
  * removed IntegrationSpec in favor of
  * ExampleSpec
  • Loading branch information...
Christopher Burnett
Christopher Burnett committed Nov 20, 2012
1 parent add4c36 commit 37c81957271dde77d4c3f6361bbae705a5142c89
@@ -17,35 +17,36 @@ package com.twitter.finatra
import com.twitter.finagle.Service
import com.twitter.util.Future
-import org.jboss.netty.buffer.ChannelBuffers.copiedBuffer
-import org.jboss.netty.handler.codec.http._
-import org.jboss.netty.handler.codec.http.HttpResponseStatus._
-import org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1
-import org.jboss.netty.util.CharsetUtil.UTF_8
import com.twitter.finagle.http.{Request => FinagleRequest, Response => FinagleResponse}
-
class AppService(controllers: ControllerCollection)
extends Service[FinagleRequest, FinagleResponse] with Logging {
- def notFoundResponse = {
- val resp = new DefaultHttpResponse(HTTP_1_1, NOT_FOUND)
- resp.setContent(copiedBuffer("not found", UTF_8))
- Future.value(FinagleResponse(resp))
- }
+ def render = new Response
+
+ def apply(rawRequest: FinagleRequest) = {
+ val adaptedRequest = RequestAdapter(rawRequest)
+ val responseConverter = new FinatraResponseConverter
+
+ try {
+ attemptRequest(rawRequest)
+ } catch {
+ case e: Exception =>
+ logger.error(e, "Internal Server Error")
+ adaptedRequest.error = Some(e)
+ responseConverter(controllers.errorHandler(adaptedRequest))
+ }
- def errorResponse = {
- val resp = new DefaultHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR)
- resp.setContent(copiedBuffer("ERROR", UTF_8))
- Future.value(FinagleResponse(resp))
}
- def apply(rawRequest: FinagleRequest) = {
+ def attemptRequest(rawRequest: FinagleRequest) = {
+ val adaptedRequest = RequestAdapter(rawRequest)
+ val responseConverter = new FinatraResponseConverter
controllers.dispatch(rawRequest) match {
case Some(response) =>
response.asInstanceOf[Future[FinagleResponse]]
case None =>
- notFoundResponse
+ responseConverter(controllers.notFoundHandler(adaptedRequest))
}
}
@@ -1,3 +1,19 @@
+/**
+ * Copyright (C) 2012 Twitter Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.twitter.finatra
object Config {
@@ -27,13 +27,23 @@ class Controller(statsReceiver: StatsReceiver = NullStatsReceiver) extends Loggi
val routes = new RouteVector[(HttpMethod, PathPattern, Request => Future[Response])]
+ var notFoundHandler: Option[(Request) => Future[Response]] = None
+ var errorHandler: Option[(Request) => Future[Response]] = None
+
def get(path: String) (callback: Request => Future[Response]) { addRoute(HttpMethod.GET, path)(callback) }
def delete(path: String)(callback: Request => Future[Response]) { addRoute(HttpMethod.DELETE, path)(callback) }
def post(path: String) (callback: Request => Future[Response]) { addRoute(HttpMethod.POST, path)(callback) }
def put(path: String) (callback: Request => Future[Response]) { addRoute(HttpMethod.PUT, path)(callback) }
def head(path: String) (callback: Request => Future[Response]) { addRoute(HttpMethod.HEAD, path)(callback) }
def patch(path: String) (callback: Request => Future[Response]) { addRoute(HttpMethod.PATCH, path)(callback) }
+ def notFound(callback: Request => Future[Response]) {
+ notFoundHandler = Option(callback)
+ }
+
+ def error(callback: Request => Future[Response]) {
+ errorHandler = Option(callback)
+ }
def dispatch(request: FinagleRequest): Option[FinagleResponse] = {
logger.info("%s %s", request.method, request.uri)
@@ -3,11 +3,22 @@ package com.twitter.finatra
import com.twitter.finagle.http.{Request => FinagleRequest, Response => FinagleResponse}
class ControllerCollection {
- var ctrls: Seq[Controller] = Seq.empty
+ var controllers: Seq[Controller] = Seq.empty
+
+ var notFoundHandler = { request:Request =>
+ render.status(404).plain("Not Found").toFuture
+ }
+
+ var errorHandler = { request:Request =>
+ render.status(500).plain("Something went wrong!").toFuture
+ }
+
+ def render = new Response
def dispatch(request: FinagleRequest):Option[FinagleResponse] = {
var response:Option[FinagleResponse] = None
- ctrls.find { ctrl =>
+
+ controllers.find { ctrl =>
ctrl.dispatch(request) match {
case Some(callbackResponse) =>
response = Some(callbackResponse)
@@ -16,11 +27,14 @@ class ControllerCollection {
false
}
}
+
response
}
def add(controller: Controller) {
- ctrls = ctrls ++ Seq(controller)
+ notFoundHandler = controller.notFoundHandler.getOrElse(notFoundHandler)
+ errorHandler = controller.errorHandler.getOrElse(errorHandler)
+ controllers = controllers ++ Seq(controller)
}
}
@@ -1,31 +0,0 @@
-package com.twitter.finatra
-
-import org.jboss.netty.util.CharsetUtil.UTF_8
-import com.twitter.finagle.{Service, SimpleFilter}
-import com.twitter.finagle.http.{Request => FinagleRequest, Response => FinagleResponse}
-import org.jboss.netty.handler.codec.http._
-import org.jboss.netty.handler.codec.http.HttpResponseStatus._
-import org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1
-import org.jboss.netty.buffer.ChannelBuffers.copiedBuffer
-
-
-class ErrorFilter extends SimpleFilter[FinagleRequest, FinagleResponse] with Logging {
- def apply(request: FinagleRequest, service: Service[FinagleRequest, FinagleResponse]) = {
-
- // `handle` asynchronously handles exceptions.
- service(request) handle { case error =>
- val statusCode = error match {
- case _: IllegalArgumentException =>
- FORBIDDEN
- case _ =>
- INTERNAL_SERVER_ERROR
- }
- val errorResponse = new DefaultHttpResponse(HTTP_1_1, statusCode)
- errorResponse.setContent(copiedBuffer("Internal Server Error".getBytes))
- logger.error(error, "Internal Server Error")
- error.printStackTrace()
-
- FinagleResponse(errorResponse)
- }
- }
-}
@@ -79,9 +79,7 @@ class FinatraServer extends Logging {
val appService = new AppService(controllers)
val fileService = new FileService
- val errorService = new ErrorFilter
- addFilter(errorService)
addFilter(fileService)
val port = Config.getInt("port")
@@ -21,8 +21,9 @@ import com.twitter.finagle.http.{Request => FinagleRequest, RequestProxy}
class Request(rawRequest: FinagleRequest) extends RequestProxy {
var multiParams: Map[String, MultipartItem] = Map.empty
- var routeParams:Map[String, String] = Map.empty
+ var routeParams: Map[String, String] = Map.empty
var request = rawRequest
+ var error: Option[Exception] = None
}
@@ -19,7 +19,7 @@ import com.twitter.util.Future
import scala.collection.Map
import org.jboss.netty.util.CharsetUtil.UTF_8
import com.twitter.finagle.http.{Request => FinagleRequest, Response => FinagleResponse}
-import com.twitter.finatra.Controller
+import com.twitter.finatra.{AppService, ControllerCollection, Controller}
import org.jboss.netty.handler.codec.http.HttpMethod
import org.scalatest.FlatSpec
import org.scalatest.matchers.ShouldMatchers
@@ -47,10 +47,12 @@ abstract class SpecHelper extends FlatSpec with ShouldMatchers {
request.httpRequest.setHeader(header._1, header._2)
}
- lastResponse = app.dispatch(request).asInstanceOf[Option[Future[FinagleResponse]]] match {
- case Some(r) => r
- case None => throw new IllegalStateException("Unable to route path '" + path +"'")
- }
+ val collection = new ControllerCollection
+ collection.add(app)
+
+ val appService = new AppService(collection)
+
+ lastResponse = appService(request)
}
def app:Controller
@@ -1,3 +1,18 @@
+/**
+ * Copyright (C) 2012 Twitter Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.twitter.finatra
import test.SpecHelper
@@ -88,6 +103,31 @@ class ExampleSpec extends SpecHelper {
val anView = new AnView
render.view(anView).toFuture
}
+
+
+ /**
+ * Custom Error Handling
+ *
+ * curl http://localhost:7070/error
+ */
+ get("/error") { request =>
+ 1234/0
+ render.plain("we never make it here").toFuture
+ }
+
+ error { request =>
+ render.status(500).plain("whoops!").toFuture
+ }
+
+
+ /**
+ * Custom 404s
+ *
+ * curl http://localhost:7070/notfound
+ */
+ notFound { request =>
+ render.status(404).plain("not found yo").toFuture
+ }
}
val app = new ExampleApp
@@ -97,6 +137,18 @@ class ExampleSpec extends SpecHelper {
/* ###BEGIN_SPEC### */
+ "GET /notfound" should "respond 404" in {
+ get("/notfound")
+ response.body should equal ("not found yo")
+ response.code should equal (404)
+ }
+
+ "GET /error" should "respond 500" in {
+ get("/error")
+ response.body should equal ("whoops!")
+ response.code should equal (500)
+ }
+
"GET /hello" should "respond with hello world" in {
get("/")
response.body should equal ("hello world")
@@ -1,76 +0,0 @@
-/**
- * Copyright (C) 2012 Twitter Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.twitter.finatra.test
-
-import com.twitter.finatra.Controller
-
-class MyApp extends Controller {
- get("/path") { request => render.plain("get:path").toFuture }
- post("/path") { request => render.plain("post:path").toFuture }
- put("/path") { request => render.plain("put:path").toFuture }
- delete("/path") { request => render.plain("delete:path").toFuture }
- patch("/path") { request => render.plain("patch:path").toFuture }
- get("/params") { request => render.plain(request.params("p")).toFuture }
- post("/params") { request => render.plain(request.params("p")).toFuture }
- get("/headers") { request => render.plain(request.headers("Referer")).toFuture }
-}
-
-class IntegrationSpec extends SpecHelper {
-
- def app = { new MyApp }
-
- "GET /path" should "respond 200" in {
- get("/path")
- response.body should equal ("get:path")
- response.code should equal (200)
- }
-
- "POST /path" should "respond 200" in {
- post("/path")
- response.body should equal ("post:path")
- }
-
- "PUT /path" should "respond 200" in {
- put("/path")
- response.body should equal ("put:path")
- }
-
- "DELETE /path" should "respond 200" in {
- delete("/path")
- response.body should equal ("delete:path")
- }
-
- "PATCH /path" should "respond 200" in {
- patch("/path")
- response.body should equal ("patch:path")
- }
-
- "GET /params" should "respond 200" in {
- get("/params", Map("p"->"yup"))
- response.body should equal ("yup")
- }
-
- "POST /params" should "respond 200" in {
- post("/params", Map("p"->"yup"))
- response.body should equal ("yup")
- }
-
- "GET /headers" should "respond 200" in {
- get("/headers", headers=Map("Referer"->"http://twitter.com"))
- response.body should equal ("http://twitter.com")
- }
-
-}

0 comments on commit 37c8195

Please sign in to comment.