Skip to content

Instantly share code, notes, and snippets.

@ComFreek
Last active October 27, 2020 12:19
Show Gist options
  • Save ComFreek/e3e53d088d53e1cf2000f2815ceb6fc0 to your computer and use it in GitHub Desktop.
Save ComFreek/e3e53d088d53e1cf2000f2815ceb6fc0 to your computer and use it in GitHub Desktop.
Make Finch servers more graceful for debugging: requests are logged + exceptions are reported to the HTTP client (instead of empty HTTP 500 response)
package com.github.comfreek.finchexample
import java.io.{PrintWriter, StringWriter}
import cats.effect.IO
import com.twitter.finagle.Service
import com.twitter.finagle.http.{Request, Response, Status}
import io.circe.{Encoder, Json}
import io.finch._
/**
* Some general-purpose boilerplate for Finch servers whose endpoints are encapsulated in an object.
*
* - succeeded and failed requests are logged to stdout
* - failed requests (due to exceptions) are served with an HTTP-500 response with a body detailing
* the exception and its stracktrace
*
* The latter greatly enhances the debugging experience.
*/
trait GracefulServerEndpoints extends Endpoint.Module[IO] {
import cats.Applicative.ops.toAllApplicativeOps
private def filters = Function.chain(Seq(exceptionLogging, logging))
private def logging: Endpoint.Compiled[IO] => Endpoint.Compiled[IO] = compiled => {
compiled.tapWithF { (req, res) =>
IO(println(s"[${res._2.map(_.statusCode).getOrElse("BAD")}] $req")) *> IO.pure(res)
}
}
private def exceptionLogging: Endpoint.Compiled[IO] => Endpoint.Compiled[IO] = compiled => {
compiled.tapWithF { (_, res) => res match {
case (trace, Left(throwable)) =>
IO(throwable.printStackTrace()) *> IO({
val newResponse = Response(Status.InternalServerError)
newResponse.write(ThrowableUtils.formatThrowableToJson(throwable).toString())
(trace, Right(newResponse))
})
case _ => IO.pure(res)
}
}
}
private object ThrowableUtils {
private def formatStackTraceToString(throwable: Throwable): String = {
val sw = new StringWriter
val pw = new PrintWriter(sw)
try {
throwable.printStackTrace(pw)
sw.toString
} finally {
sw.close()
pw.close()
}
}
def formatThrowableToJson(throwable: Throwable): Json = {
try {
Json.obj(
"message" -> Json.fromString(throwable.getMessage),
"stacktrace" -> Json.fromString(formatStackTraceToString(throwable))
)
} catch {
case _: Throwable =>
Json.obj(
"message" -> Json.fromString(
"Exception occurred during formatting a previous exception\nTo prevent further recursion down the rabbit hole, no exception information is formatted or even output this time."
)
)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment