Last active
October 27, 2020 12:19
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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