Skip to content

Commit

Permalink
Implement codec error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
asr2003 authored Nov 9, 2024
1 parent dc1f3f4 commit f75748c
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final case class ErrorResponseConfig(
withStackTrace: Boolean = false,
maxStackTraceDepth: Int = 10,
errorFormat: ErrorResponseConfig.ErrorFormat = ErrorResponseConfig.ErrorFormat.Html,
logCodecErrors: Boolean = false,
)

object ErrorResponseConfig {
Expand All @@ -34,7 +35,7 @@ object ErrorResponseConfig {

val default: ErrorResponseConfig = ErrorResponseConfig()
val debugConfig: ErrorResponseConfig =
ErrorResponseConfig(withErrorBody = true, withStackTrace = true, maxStackTraceDepth = 0)
ErrorResponseConfig(withErrorBody = true, withStackTrace = true, maxStackTraceDepth = 0, logCodecErrors = true)

private[http] val configRef: FiberRef[ErrorResponseConfig] =
FiberRef.unsafe.make(default)(Unsafe)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,23 @@ sealed trait HttpContentCodec[A] { self =>
lookup(contentType) match {
case Some((_, codec)) =>
request.body.asChunk.flatMap { bytes =>
ZIO.fromEither(codec.codec(config).decode(bytes))
ZIO
.fromEither(codec.codec(config).decode(bytes))
.mapError(_ => CodecDecodeError(s"Failed to decode request body for media type: $contentType"))
.tapError(error =>
ErrorResponseConfig.configRef.get.flatMap { errConfig =>
if (errConfig.logCodecErrors) ZIO.logWarning(error.getMessage) else ZIO.unit
},
)
}
case None =>
ZIO.fail(throw new IllegalArgumentException(s"No codec found for content type $contentType"))
ZIO
.fail(UnsupportedMediaTypeError(contentType))
.tapError(error =>
ErrorResponseConfig.configRef.get.flatMap { errConfig =>
if (errConfig.logCodecErrors) ZIO.logWarning(error.getMessage) else ZIO.unit
},
)
}
}

Expand All @@ -47,10 +60,23 @@ sealed trait HttpContentCodec[A] { self =>
lookup(contentType) match {
case Some((_, codec)) =>
response.body.asChunk.flatMap { bytes =>
ZIO.fromEither(codec.codec(config).decode(bytes))
ZIO
.fromEither(codec.codec(config).decode(bytes))
.mapError(_ => CodecDecodeError(s"Failed to decode response body for media type: $contentType"))
.tapError(error =>
ErrorResponseConfig.configRef.get.flatMap { errConfig =>
if (errConfig.logCodecErrors) ZIO.logWarning(error.getMessage) else ZIO.unit
},
)
}
case None =>
ZIO.fail(throw new IllegalArgumentException(s"No codec found for content type $contentType"))
ZIO
.fail(UnsupportedMediaTypeError(contentType))
.tapError(error =>
ErrorResponseConfig.configRef.get.flatMap { errConfig =>
if (errConfig.logCodecErrors) ZIO.logWarning(error.getMessage) else ZIO.unit
},
)
}
}

Expand Down Expand Up @@ -112,9 +138,9 @@ sealed trait HttpContentCodec[A] { self =>

private[http] def chooseFirstOrDefault(
mediaTypes: Chunk[MediaTypeWithQFactor],
): (MediaType, BinaryCodecWithSchema[A]) =
): Either[UnsupportedMediaTypeError, (MediaType, BinaryCodecWithSchema[A])] =
if (mediaTypes.isEmpty) {
(defaultMediaType, defaultBinaryCodecWithSchema)
Right((defaultMediaType, defaultBinaryCodecWithSchema))
} else {
var i = 0
var result: (MediaType, BinaryCodecWithSchema[A]) = null
Expand All @@ -124,8 +150,10 @@ sealed trait HttpContentCodec[A] { self =>
if (lookupResult.isDefined) result = lookupResult.get
i += 1
}
if (result == null) (defaultMediaType, defaultBinaryCodecWithSchema)
else result
if (result == null) {
ZIO.logWarning(s"Unsupported media type: ${mediaTypes.head.mediaType}")
Right((defaultMediaType, defaultBinaryCodecWithSchema))
} else Right(result)
}

def lookup(mediaType: MediaType): Option[(MediaType, BinaryCodecWithSchema[A])]
Expand Down Expand Up @@ -180,6 +208,15 @@ sealed trait HttpContentCodec[A] { self =>

}

sealed trait CodecError extends Throwable
case class UnsupportedMediaTypeError(mediaType: MediaType) extends CodecError {
override def getMessage: String = s"Unsupported media type: $mediaType"
}

case class CodecDecodeError(details: String) extends CodecError {
override def getMessage: String = s"Codec decode error: $details"
}

object HttpContentCodec {
final case class Choices[A](
choices: ListMap[MediaType, BinaryCodecWithSchema[A]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ private[http] object BodyCodec {

final case class Single[A](codec: HttpContentCodec[A], name: Option[String]) extends BodyCodec[A] {

def mediaType(accepted: Chunk[MediaTypeWithQFactor]): Option[MediaType] =
Some(codec.chooseFirstOrDefault(accepted)._1)

def mediaType(accepted: Chunk[MediaTypeWithQFactor]): Option[MediaType] =
codec.chooseFirstOrDefault(accepted) match {
case Right((mediaType, _)) => Some(mediaType)
case Left(_) => None
}
def decodeFromField(field: FormField, config: CodecConfig)(implicit trace: Trace): IO[Throwable, A] = {
val codec0 = codec
.lookup(field.contentType)
Expand Down Expand Up @@ -139,27 +141,36 @@ private[http] object BodyCodec {
def encodeToField(value: A, mediaTypes: Chunk[MediaTypeWithQFactor], name: String, config: CodecConfig)(implicit
trace: Trace,
): FormField = {
val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes)
if (mediaType.binary) {
FormField.binaryField(
name,
bc.codec(config).encode(value),
mediaType,
)
} else {
FormField.textField(
name,
bc.codec(config).encode(value).asString,
mediaType,
)
codec.chooseFirstOrDefault(mediaTypes) match {

case Right((mediaType, bc @ BinaryCodecWithSchema(_, _))) =>
if (mediaType.binary) {
FormField.binaryField(
name,
bc.codec(config).encode(value),
mediaType,
)
} else {
FormField.textField(
name,
bc.codec(config).encode(value).asString,
mediaType,
)
}
case Left(error) =>
throw new IllegalArgumentException(s"Unsupported media type: ${error.mediaType}")
}
}

def encodeToBody(value: A, mediaTypes: Chunk[MediaTypeWithQFactor], config: CodecConfig)(implicit
trace: Trace,
): Body = {
val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes)
Body.fromChunk(bc.codec(config).encode(value), mediaType)
codec.chooseFirstOrDefault(mediaTypes) match {
case Right((mediaType, bc @ BinaryCodecWithSchema(_, _))) =>
Body.fromChunk(bc.codec(config).encode(value), mediaType)
case Left(error) =>
throw new IllegalArgumentException(s"Unsupported media type: ${error.mediaType}")
}
}

type Element = A
Expand All @@ -169,8 +180,10 @@ private[http] object BodyCodec {
extends BodyCodec[ZStream[Any, Nothing, E]] {

def mediaType(accepted: Chunk[MediaTypeWithQFactor]): Option[MediaType] =
Some(codec.chooseFirstOrDefault(accepted)._1)

codec.chooseFirstOrDefault(accepted) match {
case Right((mediaType, _)) => Some(mediaType)
case Left(_) => None
}
override def decodeFromField(field: FormField, config: CodecConfig)(implicit
trace: Trace,
): IO[Throwable, ZStream[Any, Nothing, E]] =
Expand Down Expand Up @@ -200,12 +213,16 @@ private[http] object BodyCodec {
)(implicit
trace: Trace,
): FormField = {
val (mediaType, bc) = codec.chooseFirstOrDefault(mediaTypes)
FormField.streamingBinaryField(
name,
value >>> bc.codec(config).streamEncoder,
mediaType,
)
codec.chooseFirstOrDefault(mediaTypes) match {
case Right((mediaType, bc)) =>
FormField.streamingBinaryField(
name,
value >>> bc.codec(config).streamEncoder,
mediaType,
)
case Left(error) =>
throw new IllegalArgumentException(s"Unsupported media type: ${error.mediaType}")
}
}

override def encodeToBody(
Expand All @@ -215,8 +232,12 @@ private[http] object BodyCodec {
)(implicit
trace: Trace,
): Body = {
val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes)
Body.fromStreamChunked(value >>> bc.codec(config).streamEncoder).contentType(mediaType)
codec.chooseFirstOrDefault(mediaTypes) match {
case Right((mediaType, bc @ BinaryCodecWithSchema(_, _))) =>
Body.fromStreamChunked(value >>> bc.codec(config).streamEncoder).contentType(mediaType)
case Left(error) =>
throw new IllegalArgumentException(s"Unsupported media type: ${error.mediaType}")
}
}

type Element = E
Expand Down

0 comments on commit f75748c

Please sign in to comment.