diff --git a/modules/core/src/smithy4s/http/HttpResponse.scala b/modules/core/src/smithy4s/http/HttpResponse.scala index cdd3d7a2e..770eb9425 100644 --- a/modules/core/src/smithy4s/http/HttpResponse.scala +++ b/modules/core/src/smithy4s/http/HttpResponse.scala @@ -247,11 +247,9 @@ object HttpResponse { response.statusCode, response.headers, bodyBlob.toUTF8String, - Some( - FailedDecodeAttempt( - discriminator = discriminator, - contractError = error - ) + FailedDecodeAttempt.DecodingFailure( + discriminator = discriminator, + contractError = error ) ) ) @@ -263,7 +261,10 @@ object HttpResponse { code = response.statusCode, headers = response.headers, body = bodyBlob.toUTF8String, - failedDecodeAttempt = None + failedDecodeAttempt = + FailedDecodeAttempt.UnrecognisedDiscriminator( + discriminator + ) ) ) } diff --git a/modules/core/src/smithy4s/http/RawErrorResponse.scala b/modules/core/src/smithy4s/http/RawErrorResponse.scala index 6317d27c4..0c7eafbc5 100644 --- a/modules/core/src/smithy4s/http/RawErrorResponse.scala +++ b/modules/core/src/smithy4s/http/RawErrorResponse.scala @@ -20,24 +20,37 @@ case class RawErrorResponse( code: Int, headers: Map[CaseInsensitive, Seq[String]], body: String, - failedDecodeAttempt: Option[FailedDecodeAttempt] + failedDecodeAttempt: FailedDecodeAttempt ) extends Throwable { override def getMessage(): String = { val baseMessage = s"status $code, headers: $headers, body:\n$body" - failedDecodeAttempt match { - case Some(attempt) => - baseMessage + - s""" - |FailedDecodeAttempt: - | discriminator: ${attempt.discriminator} - | contractError: ${attempt.contractError.getMessage} + baseMessage + + s""" + |FailedDecodeAttempt: + | ${failedDecodeAttempt.getMessage} """.stripMargin - case None => baseMessage - } } + + override def getCause: Throwable = failedDecodeAttempt +} + +sealed trait FailedDecodeAttempt extends Throwable { + def discriminator: HttpDiscriminator + def getMessage: String } -case class FailedDecodeAttempt( - discriminator: HttpDiscriminator, - contractError: HttpContractError -) +object FailedDecodeAttempt { + case class UnrecognisedDiscriminator(discriminator: HttpDiscriminator) + extends FailedDecodeAttempt { + override def getMessage: String = + s"Unrecognised descriminator: $discriminator" + } + + case class DecodingFailure( + discriminator: HttpDiscriminator, + contractError: HttpContractError + ) extends FailedDecodeAttempt { + override def getMessage: String = + s"Decoding failed for discriminator: $discriminator with error: ${contractError.getMessage}" + } +} diff --git a/modules/tests/src/smithy4s/tests/PizzaClientSpec.scala b/modules/tests/src/smithy4s/tests/PizzaClientSpec.scala index f0be8c9af..38b01e0f6 100644 --- a/modules/tests/src/smithy4s/tests/PizzaClientSpec.scala +++ b/modules/tests/src/smithy4s/tests/PizzaClientSpec.scala @@ -114,61 +114,67 @@ abstract class PizzaClientSpec extends IOSuite { GenericClientError("generic error message for 418") ) - clientTestForError( - "Handle error with a discriminator but can't be decoded", + clientAssertError[RawErrorResponse]( + "Handle error with a discriminator but can't be decoded", Response[IO](status = Status.NotFound) .withEntity("malformed body") .withHeaders(Header.Raw(CIString("X-Error-Type"), "NotFoundError")), - rawErrorResponse( - 404, - Map("X-Error-Type" -> "NotFoundError"), - "malformed body", - Some( - FailedDecodeAttempt( - discriminator = HttpDiscriminator.NameOnly("NotFoundError"), - contractError = HttpPayloadError( - path = smithy4s.codecs.PayloadPath(List()), - expected = "object", - message = - """Expected JSON object, offset: 0x00000000, buf: - |+----------+-------------------------------------------------+------------------+ - || | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 0123456789abcdef | - |+----------+-------------------------------------------------+------------------+ - || 00000000 | 6d 61 6c 66 6f 72 6d 65 64 20 62 6f 64 79 | malformed body | - |+----------+-------------------------------------------------+------------------+""".stripMargin - ) - ) - ) - ) + error => { + error match { + case RawErrorResponse( + code, + headers, + body, + FailedDecodeAttempt.DecodingFailure( + discriminator, + HttpPayloadError(path, expected, _) + ) + ) => + expect(code == 404) && + expect( + headers == Map( + CaseInsensitive("X-Error-Type") -> List("NotFoundError") + ) + ) && + expect(body == "malformed body") && + expect( + discriminator == HttpDiscriminator.NameOnly("NotFoundError") + ) && + expect(path == smithy4s.codecs.PayloadPath(List())) && + expect(expected == "object") + case _ => failure("Unexpected error type or values") + } + } ) - clientTestForError( + clientAssertError[RawErrorResponse]( "Handle malformed error response with no discriminator", Response(status = Status.InternalServerError) .withEntity("goodbye world"), - rawErrorResponse( - code = 500, - headers = Map( - "Content-Length" -> "13", - "Content-Type" -> "text/plain; charset=UTF-8" - ), - body = "goodbye world", - failedDecodeAttempt = None - ) - ) + error => { + error match { + case RawErrorResponse( + code, + headers, + body, + FailedDecodeAttempt.UnrecognisedDiscriminator(discriminator) + ) => + expect(code == 500) && + expect( + headers == Map( + CaseInsensitive("Content-Length") -> List("13"), + CaseInsensitive("Content-Type") -> List( + "text/plain; charset=UTF-8" + ) + ) + ) && + expect(body == "goodbye world") && + expect(discriminator == HttpDiscriminator.StatusCode(500)) + case _ => failure("Unexpected error type or values") + } - private def rawErrorResponse( - code: Int, - headers: Map[String, String], - body: String, - failedDecodeAttempt: Option[FailedDecodeAttempt] - ): RawErrorResponse = - RawErrorResponse( - code, - headers.map { case (k, v) => CaseInsensitive(k) -> List(v) }, - body, - failedDecodeAttempt - ) + } + ) clientTest("Headers are case insensitive") { (client, backend, log) => for { @@ -243,6 +249,30 @@ abstract class PizzaClientSpec extends IOSuite { } } + def clientAssertError[E]( + name: String, + response: Response[IO], + assert: E => Expectations + )(implicit + loc: SourceLocation, + ct: scala.reflect.ClassTag[E] + ) = { + clientTest(name) { (client, backend, log) => + for { + _ <- backend.prepResponse(name, response) + maybeResult <- client.getMenu(name).attempt + + } yield maybeResult match { + case Right(_) => failure("expected failure") + case Left(error: E) => + assert(error) + case Left(error) => + failure(s"Error of unexpected type: $error") + } + } + + } + def clientTest(name: TestName)( f: ( PizzaAdminService[IO], diff --git a/modules/tests/src/smithy4s/tests/PizzaSpec.scala b/modules/tests/src/smithy4s/tests/PizzaSpec.scala index f05f19fdf..033075ab3 100644 --- a/modules/tests/src/smithy4s/tests/PizzaSpec.scala +++ b/modules/tests/src/smithy4s/tests/PizzaSpec.scala @@ -34,6 +34,7 @@ import smithy4s.example.PizzaAdminService import smithy4s.http.CaseInsensitive import smithy4s.http.HttpContractError import smithy4s.http.HttpDiscriminator +import smithy4s.http.FailedDecodeAttempt import smithy4s.http.RawErrorResponse import weaver._ import cats.Show @@ -509,10 +510,18 @@ abstract class PizzaSpec CaseInsensitive("Content-Length") -> List("14") ) response match { - case Left(RawErrorResponse(code, headers, body, None)) => + case Left( + RawErrorResponse(code, headers, body, failedDecodeAttempt) + ) => expect(code == 500) && expect(headers == expectHeaders) && - expect(body.contains("malformed body")) + expect(body.contains("malformed body")) && + expect( + failedDecodeAttempt == FailedDecodeAttempt + .UnrecognisedDiscriminator( + HttpDiscriminator.StatusCode(500) + ) + ) case _ => failure("Expected RawErrorResponse with status 500") } @@ -553,17 +562,6 @@ abstract class PizzaSpec } } yield { - val expectedHttpPayloadError = HttpPayloadError( - path = smithy4s.codecs.PayloadPath(List()), - expected = "object", - message = - """Expected JSON object, offset: 0x00000000, buf: - |+----------+-------------------------------------------------+------------------+ - || | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 0123456789abcdef | - |+----------+-------------------------------------------------+------------------+ - || 00000000 | 6d 61 6c 66 6f 72 6d 65 64 20 62 6f 64 79 | malformed body | - |+----------+-------------------------------------------------+------------------+""".stripMargin - ) val expectHeaders = Map( CaseInsensitive("Content-Length") -> List("14"), CaseInsensitive("Content-Type") -> List("text/plain"), @@ -571,7 +569,7 @@ abstract class PizzaSpec ) response match { case Left( - RawErrorResponse(code, headers, body, Some(failedDecodeAttempt)) + RawErrorResponse(code, headers, body, failedDecodeAttempt) ) => expect(code == 500) && expect(headers == expectHeaders) && @@ -580,9 +578,6 @@ abstract class PizzaSpec failedDecodeAttempt.discriminator == HttpDiscriminator.NameOnly( "GenericServerError" ) - ) && - expect( - failedDecodeAttempt.contractError == expectedHttpPayloadError ) case _ => failure("Expected RawErrorResponse with status 500")