diff --git a/core/src/main/scala/sttp/client4/ResponseAs.scala b/core/src/main/scala/sttp/client4/ResponseAs.scala index 9cf6c4a5e..2af01a0d3 100644 --- a/core/src/main/scala/sttp/client4/ResponseAs.scala +++ b/core/src/main/scala/sttp/client4/ResponseAs.scala @@ -1,14 +1,21 @@ package sttp.client4 -import sttp.capabilities.{Effect, Streams, WebSockets} +import sttp.capabilities.Effect +import sttp.capabilities.Streams +import sttp.capabilities.WebSockets +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode import sttp.client4.internal.SttpFile import sttp.model.ResponseMetadata import sttp.model.internal.Rfc3986 -import sttp.ws.{WebSocket, WebSocketFrame} +import sttp.ws.WebSocket +import sttp.ws.WebSocketFrame import java.io.InputStream import scala.collection.immutable.Seq -import scala.util.{Failure, Success, Try} +import scala.util.Failure +import scala.util.Success +import scala.util.Try /** Describes how the response body of a request should be handled. A number of `as` helper methods are available * as part of [[SttpApi]] and when importing `sttp.client4._`. These methods yield specific implementations of this @@ -77,28 +84,28 @@ case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseA ) /** If the type to which the response body should be deserialized is an `Either[A, B]`: - * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not - * yet an exception) + * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if + * `A` is not yet an exception) * - in case of `B`, returns the value directly */ def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): ResponseAs[B] = mapWithMetadata { case (t, meta) => (t: Either[A, B]) match { case Left(a: Exception) => throw a - case Left(a) => throw HttpError(a, meta) + case Left(a) => throw UnexpectedStatusCode(a, meta) case Right(b) => b } } /** If the type to which the response body should be deserialized is an `Either[ResponseException[HE], B]`, either * throws / returns a failed effect with the [[DeserializationException]], returns the deserialized body from the - * [[HttpError]], or the deserialized successful body `B`. + * [[UnexpectedStatusCode]], or the deserialized successful body `B`. */ def orFailDeserialization[HE, B](implicit tIsEither: T <:< Either[ResponseException[HE], B] ): ResponseAs[Either[HE, B]] = map { t => (t: Either[ResponseException[HE], B]) match { - case Left(HttpError(he, _)) => Left(he) + case Left(UnexpectedStatusCode(he, _)) => Left(he) case Left(d: DeserializationException) => throw d case Right(b) => Right(b) } @@ -109,13 +116,13 @@ case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseA object ResponseAs { - /** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using - * the given function, catching any exceptions and representing them as [[DeserializationException]] s. + /** Returns a function, which maps `Left` values to [[UnexpectedStatusCode]] s, and attempts to deserialize `Right` + * values using the given function, catching any exceptions and representing them as [[DeserializationException]] s. */ def deserializeRightCatchingExceptions[T]( doDeserialize: String => T ): (Either[String, String], ResponseMetadata) => Either[ResponseException[String], T] = { - case (Left(s), meta) => Left(HttpError(s, meta)) + case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta)) case (Right(s), meta) => deserializeCatchingExceptions(doDeserialize)(s, meta) } @@ -133,13 +140,13 @@ object ResponseAs { } ) - /** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using - * the given function. + /** Returns a function, which maps `Left` values to [[UnexpectedStatusCode]] s, and attempts to deserialize `Right` + * values using the given function. */ def deserializeRightWithError[T]( doDeserialize: String => Either[Exception, T] ): (Either[String, String], ResponseMetadata) => Either[ResponseException[String], T] = { - case (Left(s), meta) => Left(HttpError(s, meta)) + case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta)) case (Right(s), meta) => deserializeWithError(doDeserialize)(s, meta) } @@ -228,15 +235,15 @@ case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends Re StreamResponseAs(delegate.mapWithMetadata(f)) /** If the type to which the response body should be deserialized is an `Either[A, B]`: - * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not - * yet an exception) + * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if + * `A` is not yet an exception) * - in case of `B`, returns the value directly */ def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): StreamResponseAs[B, S] = mapWithMetadata { case (t, meta) => (t: Either[A, B]) match { case Left(a: Exception) => throw a - case Left(a) => throw HttpError(a, meta) + case Left(a) => throw UnexpectedStatusCode(a, meta) case Right(b) => b } } @@ -269,15 +276,15 @@ case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F WebSocketResponseAs(delegate.mapWithMetadata(f)) /** If the type to which the response body should be deserialized is an `Either[A, B]`: - * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not - * yet an exception) + * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if + * `A` is not yet an exception) * - in case of `B`, returns the value directly */ def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketResponseAs[F, B] = mapWithMetadata { case (t, meta) => (t: Either[A, B]) match { case Left(a: Exception) => throw a - case Left(a) => throw HttpError(a, meta) + case Left(a) => throw UnexpectedStatusCode(a, meta) case Right(b) => b } } @@ -310,15 +317,15 @@ case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S wit WebSocketStreamResponseAs[T2, S](delegate.mapWithMetadata(f)) /** If the type to which the response body should be deserialized is an `Either[A, B]`: - * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not - * yet an exception) + * - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if + * `A` is not yet an exception) * - in case of `B`, returns the value directly */ def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketStreamResponseAs[B, S] = mapWithMetadata { case (t, meta) => (t: Either[A, B]) match { case Left(a: Exception) => throw a - case Left(a) => throw HttpError(a, meta) + case Left(a) => throw UnexpectedStatusCode(a, meta) case Right(b) => b } } diff --git a/core/src/main/scala/sttp/client4/ResponseException.scala b/core/src/main/scala/sttp/client4/ResponseException.scala index 609b53ed3..96dc3fd29 100644 --- a/core/src/main/scala/sttp/client4/ResponseException.scala +++ b/core/src/main/scala/sttp/client4/ResponseException.scala @@ -7,10 +7,10 @@ import sttp.model.ResponseMetadata * successfully. * * A response exception can itself be one of two cases: - * - a [[HttpError]], when the response code is other than 2xx (or whatever is considered "success" by the response - * handling description); the body is deserialized to `HE` - * - a [[DeserializationException]], when there's an error during deserialization (this includes deserialization - * exceptions of both the success and error branches) + * - [[ResponseException.UnexpectedStatusCode]], when the response code is other than 2xx (or whatever is considered + * "success" by the response handling description); the body is deserialized to `HE` + * - [[ResponseException.DeserializationException]], when there's an error during deserialization (this includes + * deserialization exceptions of both the success and error branches) * * This type is often used as the left-side of a top-level either (where the right-side represents a successful request * and deserialization). When thrown/returned when sending a request (e.g. in `...OrFailed` response handling @@ -26,24 +26,27 @@ sealed abstract class ResponseException[+HE]( val response: ResponseMetadata ) extends Exception(error, cause.orNull) -/** Represents an http error, where the response was received successfully, but the status code is other than the - * expected one (typically other than 2xx). - * - * @tparam HE - * The type of the body to which the error response is deserialized. - */ -case class HttpError[+HE](body: HE, override val response: ResponseMetadata) - extends ResponseException[HE](s"statusCode: ${response.code}, response: $body", None, response) +object ResponseException { -/** Represents an error that occurred during deserialization of `body`. */ -case class DeserializationException(body: String, cause: Exception, override val response: ResponseMetadata) - extends ResponseException[Nothing]( - cause.getMessage(), - Some(cause), - response - ) + /** Represents an error, where the response was received successfully, but the status code is other than the expected + * one (typically other than 2xx). + * + * @tparam HE + * The type of the body to which the error response is deserialized. + */ + case class UnexpectedStatusCode[+HE](body: HE, override val response: ResponseMetadata) + extends ResponseException[HE](s"statusCode: ${response.code}, response: $body", None, response) + + /** Represents an error that occurred during deserialization of `body`. */ + case class DeserializationException(body: String, cause: Exception, override val response: ResponseMetadata) + extends ResponseException[Nothing]( + cause.getMessage(), + Some(cause), + response + ) + + // -object ResponseException { @tailrec def find(exception: Throwable): Option[ResponseException[_]] = Option(exception) match { case Some(e: ResponseException[_]) => Some(e) diff --git a/core/src/main/scala/sttp/client4/SttpApi.scala b/core/src/main/scala/sttp/client4/SttpApi.scala index b6a467cc8..da9e9e86f 100644 --- a/core/src/main/scala/sttp/client4/SttpApi.scala +++ b/core/src/main/scala/sttp/client4/SttpApi.scala @@ -54,8 +54,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator { val basicRequest: PartialRequest[Either[String, String]] = emptyRequest.acceptEncoding("gzip, deflate") - /** A starting request which always reads the response body as a string, if the response code is successfull (2xx), - * and fails (throws an exception, or returns a failed effect) otherwise. + /** A starting request which always reads the response body as a string, if the response code is successful (2xx), and + * fails (throws an exception, or returns a failed effect) otherwise. */ val quickRequest: PartialRequest[String] = basicRequest.response(asStringOrFail) @@ -96,8 +96,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator { } .showAs("as string") - /** Reads the response as a `String`, if the status code is 2xx. Otherwise, throws an [[HttpError]] / returns a failed - * effect. Use the `utf-8` charset by default, unless specified otherwise in the response headers. + /** Reads the response as a `String`, if the status code is 2xx. Otherwise, throws an [[UnexpectedStatusCode]] / + * returns a failed effect. Use the `utf-8` charset by default, unless specified otherwise in the response headers. * * @see * the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into @@ -116,7 +116,7 @@ trait SttpApi extends SttpExtensions with UriInterpolator { def asByteArrayAlways: ResponseAs[Array[Byte]] = ResponseAs(ResponseAsByteArray) /** Reads the response as an array of bytes, without any processing, if the status code is 2xx. Otherwise, throws an - * [[HttpError]] / returns a failed effect. + * [[UnexpectedStatusCode]] / returns a failed effect. * * @see * the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into @@ -148,8 +148,9 @@ trait SttpApi extends SttpExtensions with UriInterpolator { asStringAlways(charset2).map(GenericResponseAs.parseParams(_, charset2)).showAs("as params") } - /** Deserializes the response as form parameters, if the status code is 2xx. Otherwise, throws an [[HttpError]] / - * returns a failed effect. Uses the `utf-8` charset by default, unless specified otherwise in the response headers. + /** Deserializes the response as form parameters, if the status code is 2xx. Otherwise, throws an + * [[UnexpectedStatusCode]] / returns a failed effect. Uses the `utf-8` charset by default, unless specified + * otherwise in the response headers. * * @see * the [[ResponseAs#orFail]] method can be used to convert any response description which returns an `Either` into @@ -284,8 +285,8 @@ trait SttpApi extends SttpExtensions with UriInterpolator { asEither(asStringAlways, asStreamAlways(s)(f)) /** Handles the response body by providing a stream with the response's data to `f`, if the status code is 2xx. - * Otherwise, returns a failed effect (with [[HttpError]]). The effect type used by `f` must be compatible with the - * effect type of the backend. The stream is always closed after `f` completes. + * Otherwise, returns a failed effect (with [[UnexpectedStatusCode]]). The effect type used by `f` must be compatible + * with the effect type of the backend. The stream is always closed after `f` completes. * * A non-blocking, asynchronous streaming implementation must be provided as the [[Streams]] parameter. * diff --git a/core/src/main/scala/sttp/client4/SttpClientException.scala b/core/src/main/scala/sttp/client4/SttpClientException.scala index 444d180e6..cf2df5739 100644 --- a/core/src/main/scala/sttp/client4/SttpClientException.scala +++ b/core/src/main/scala/sttp/client4/SttpClientException.scala @@ -23,7 +23,7 @@ import sttp.monad.MonadError * @param cause * The original exception. */ -abstract class SttpClientException(val request: GenericRequest[_, _], val cause: Exception) +sealed abstract class SttpClientException(val request: GenericRequest[_, _], val cause: Exception) extends Exception(s"Exception when sending request: ${request.method} ${request.uri}", cause) object SttpClientException extends SttpClientExceptionExtensions { @@ -31,6 +31,8 @@ object SttpClientException extends SttpClientExceptionExtensions { class ReadException(request: GenericRequest[_, _], cause: Exception) extends SttpClientException(request, cause) + // + class TimeoutException(request: GenericRequest[_, _], cause: Exception) extends ReadException(request, cause) class TooManyRedirectsException(request: GenericRequest[_, _], val redirects: Int) @@ -42,6 +44,8 @@ object SttpClientException extends SttpClientExceptionExtensions { class ResponseHandlingException[+HE](request: GenericRequest[_, _], val responseException: ResponseException[HE]) extends ReadException(request, responseException) + // + def adjustExceptions[F[_], T]( monadError: MonadError[F] )(t: => F[T])(usingFn: Exception => Option[Exception]): F[T] = diff --git a/core/src/main/scala/sttp/client4/SttpWebSocketAsyncApi.scala b/core/src/main/scala/sttp/client4/SttpWebSocketAsyncApi.scala index 2176905a8..efe6b7f2f 100644 --- a/core/src/main/scala/sttp/client4/SttpWebSocketAsyncApi.scala +++ b/core/src/main/scala/sttp/client4/SttpWebSocketAsyncApi.scala @@ -15,7 +15,7 @@ trait SttpWebSocketAsyncApi { asWebSocketEither(asStringAlways, asWebSocketAlways(f)) /** Handles the response as a web socket, providing an open [[WebSocket]] instance to the `f` function, if the status - * code is 2xx. Otherwise, returns a failed effect (with [[HttpError]]). + * code is 2xx. Otherwise, returns a failed effect (with [[]]). * * The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always * closed after `f` completes. diff --git a/core/src/main/scala/sttp/client4/SttpWebSocketStreamApi.scala b/core/src/main/scala/sttp/client4/SttpWebSocketStreamApi.scala index cdb064076..3fc68e9fd 100644 --- a/core/src/main/scala/sttp/client4/SttpWebSocketStreamApi.scala +++ b/core/src/main/scala/sttp/client4/SttpWebSocketStreamApi.scala @@ -20,7 +20,7 @@ trait SttpWebSocketStreamApi { /** Handles the response as a web socket, using the given `p` stream processing pipe to handle the incoming & produce * the outgoing web socket frames, if the status code is 2xx. Otherwise, returns a failed effect (with - * [[HttpError]]). + * [[UnexpectedStatusCode]]). * * The effect type used by `f` must be compatible with the effect type of the backend. The web socket is always * closed after `p` completes. diff --git a/core/src/main/scala/sttp/client4/SttpWebSocketSyncApi.scala b/core/src/main/scala/sttp/client4/SttpWebSocketSyncApi.scala index 09f504f69..0401a330e 100644 --- a/core/src/main/scala/sttp/client4/SttpWebSocketSyncApi.scala +++ b/core/src/main/scala/sttp/client4/SttpWebSocketSyncApi.scala @@ -16,7 +16,7 @@ trait SttpWebSocketSyncApi { asWebSocketEither(asStringAlways, asWebSocketAlways(f)) /** Handles the response as a web socket, providing an open [[WebSocket]] instance to the `f` function, if the status - * code is 2xx. Otherwise, throws an [[HttpError]]. + * code is 2xx. Otherwise, throws an [[UnexpectedStatusCode]]. * * The web socket is always closed after `f` completes. * diff --git a/core/src/test/scala/sttp/client4/LogTests.scala b/core/src/test/scala/sttp/client4/LogTests.scala index 1c88cfdd0..459111508 100644 --- a/core/src/test/scala/sttp/client4/LogTests.scala +++ b/core/src/test/scala/sttp/client4/LogTests.scala @@ -10,6 +10,7 @@ import sttp.shared.Identity import scala.collection.immutable.Seq import scala.collection.mutable import sttp.client4.testing.ResponseStub +import sttp.client4.ResponseException.DeserializationException class LogTests extends AnyFlatSpec with Matchers with BeforeAndAfter { private class SpyLogger extends Logger[Identity] { diff --git a/core/src/test/scala/sttp/client4/testing/BackendStubTests.scala b/core/src/test/scala/sttp/client4/testing/BackendStubTests.scala index 04f51696f..edcfa56a8 100644 --- a/core/src/test/scala/sttp/client4/testing/BackendStubTests.scala +++ b/core/src/test/scala/sttp/client4/testing/BackendStubTests.scala @@ -3,12 +3,16 @@ package sttp.client4.testing import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers -import sttp.client4.SttpClientException.ReadException import sttp.client4._ +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.SttpClientException.ReadException import sttp.client4.internal._ import sttp.client4.ws.async._ import sttp.model._ -import sttp.monad.{FutureMonad, IdentityMonad, MonadError, TryMonad} +import sttp.monad.FutureMonad +import sttp.monad.IdentityMonad +import sttp.monad.MonadError +import sttp.monad.TryMonad import sttp.shared.Identity import sttp.ws.WebSocketFrame import sttp.ws.testing.WebSocketStub @@ -18,7 +22,9 @@ import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} +import scala.util.Failure +import scala.util.Success +import scala.util.Try class BackendStubTests extends AnyFlatSpec with Matchers with ScalaFutures { private val testingStub = SyncBackendStub @@ -114,8 +120,8 @@ class BackendStubTests extends AnyFlatSpec with Matchers with ScalaFutures { ) .send(testingBackend) - val readException = the[sttp.client4.SttpClientException.ReadException] thrownBy request() - readException.cause shouldBe a[sttp.client4.DeserializationException] + val readException = the[SttpClientException.ReadException] thrownBy request() + readException.cause shouldBe a[DeserializationException] } it should "use rules in partial function" in { diff --git a/core/src/test/scala/sttp/client4/testing/HttpTest.scala b/core/src/test/scala/sttp/client4/testing/HttpTest.scala index 6fa75884f..567688c8a 100644 --- a/core/src/test/scala/sttp/client4/testing/HttpTest.scala +++ b/core/src/test/scala/sttp/client4/testing/HttpTest.scala @@ -183,7 +183,7 @@ trait HttpTest[F[_]] } .toFuture() .map( - _ shouldBe "sttp.client4.HttpError: statusCode: 400, response: POST /echo/custom_status/400 this is the body" + _ shouldBe "sttp.client4.ResponseException.UnexpectedStatusCode: statusCode: 400, response: POST /echo/custom_status/400 this is the body" ) } diff --git a/core/src/test/scala/sttp/client4/testing/streaming/StreamingTest.scala b/core/src/test/scala/sttp/client4/testing/streaming/StreamingTest.scala index 824307a20..8af2228c4 100644 --- a/core/src/test/scala/sttp/client4/testing/streaming/StreamingTest.scala +++ b/core/src/test/scala/sttp/client4/testing/streaming/StreamingTest.scala @@ -286,7 +286,7 @@ abstract class StreamingTest[F[_], S] } .toFuture() .map( - _ shouldBe "sttp.client4.HttpError: statusCode: 400, response: POST /echo/custom_status/400 streaming test" + _ shouldBe "sttp.client4.ResponseException.UnexpectedStatusCode: statusCode: 400, response: POST /echo/custom_status/400 streaming test" ) } diff --git a/docs/migrate_v3_v4.md b/docs/migrate_v3_v4.md index 8427152b2..b594b4298 100644 --- a/docs/migrate_v3_v4.md +++ b/docs/migrate_v3_v4.md @@ -42,4 +42,5 @@ Any `Either`-based response description can be converted to a failing one using * `async-http-client` backends are removed (as there's no reactive streams support in v3, making integration difficult) * request attributes replace request tags (same mechanism as in Tapir) * the parametrization of `ResponseException` is simplified, `DeserializationException` does not have a type parameter, always requiring an `Exception` as the cause instead -* when a `DeserializationException` is thrown in the response handling specification, this will be logged as a successful response (as the response was received correctly), and counted as a success in the metrics as well \ No newline at end of file +* when a `ResponseException` is thrown in the response handling specification, this will be logged as a successful response (as the response was received correctly), and counted as a success in the metrics as well +* `HttpError` is renamed to `UnexpectedStatusCode`, and along with `DeserializationException`, both types are nested within `ResponseException` \ No newline at end of file diff --git a/docs/responses/body.md b/docs/responses/body.md index ebc7eb7c0..0059810e3 100644 --- a/docs/responses/body.md +++ b/docs/responses/body.md @@ -91,10 +91,10 @@ import sttp.client4.* basicRequest.response(asStringOrFail): PartialRequest[String] ``` -The `.orFail` combinator works in all cases where the response body is specified to be deserialized as an `Either`. If the left is already an exception, it will be thrown unchanged. Otherwise, the left-value will be wrapped in an `HttpError`. +The `.orFail` combinator works in all cases where the response body is specified to be deserialized as an `Either`. If the left is already an exception, it will be thrown unchanged. Otherwise, the left-value will be wrapped in an `UnexpectedStatusCode`. ```{note} -While both ``asStringAlways`` and ``asStringOrFail`` have the type ``ResponseAs[String]``, they are different. The first will return the response body as a string always, regardless of the responses' status code. The second will return a failed effect / throw a ``HttpError`` exception for non-2xx status codes, and the string as body only for 2xx status codes. +While both `asStringAlways` and `asStringOrFail` have the type `ResponseAs[String]`, they are different. The first will return the response body as a string always, regardless of the responses' status code. The second will return a failed effect / throw a `UnexpectedStatusCode` exception for non-2xx status codes, and the string as body only for 2xx status codes. ``` There's also a variant of the combinator, `.orFailDeserialization`, which can be used to extract typed errors and fail the effect if there's a deserialization error. diff --git a/docs/responses/exceptions.md b/docs/responses/exceptions.md index 203ccbfcc..5939d80cf 100644 --- a/docs/responses/exceptions.md +++ b/docs/responses/exceptions.md @@ -24,7 +24,7 @@ Unknown exceptions aren't categorized and are re-thrown unchanged. ## Response-handling, deserialization errors -Exceptions might be thrown when deserializing the response body - depending on the specification of how to handle response bodies. The built-in deserializers (see e.g. [json](../other/json.md)) return errors represented as `ResponseException[HE]`, which can either be a `HttpError` (protocol-level failures, containing a potentially deserialized body value) or a `DeserializationException` (containing a deserialization-library-specific exception). +Exceptions might be thrown when deserializing the response body - depending on the specification of how to handle response bodies. The built-in deserializers (see e.g. [json](../other/json.md)) return errors represented as `ResponseException[HE]`, which can either be a `UnexpectedStatusCode` (protocol-level failures, containing a potentially deserialized body value) or a `DeserializationException` (containing a deserialization-library-specific exception). This means that a typical `asJson` response specification will result in the body being read as: diff --git a/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala b/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala index 75b6b902c..44c19da0c 100644 --- a/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala +++ b/json/circe/src/main/scala/sttp/client4/circe/SttpCirceApi.scala @@ -1,12 +1,16 @@ package sttp.client4.circe -import sttp.client4._ +import io.circe.Decoder +import io.circe.Encoder +import io.circe.Printer import io.circe.parser.decode -import io.circe.{Decoder, Encoder, Printer} +import sttp.client4._ +import sttp.client4.ResponseAs.deserializeEitherWithErrorOrThrow +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode import sttp.client4.internal.Utf8 -import sttp.model.MediaType import sttp.client4.json._ -import sttp.client4.ResponseAs.deserializeEitherWithErrorOrThrow +import sttp.model.MediaType trait SttpCirceApi { @@ -19,7 +23,8 @@ trait SttpCirceApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: Decoder: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -41,14 +46,14 @@ trait SttpCirceApi { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: Decoder: IsOption, B: Decoder: IsOption]: ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), UnexpectedStatusCode(_, meta)) case de @ DeserializationException(_, _, _) => de } }.showAsJsonEither diff --git a/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala b/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala index 699a170c0..fc51c7c62 100644 --- a/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala +++ b/json/circe/src/test/scala/sttp/client4/circe/CirceTests.scala @@ -8,6 +8,7 @@ import sttp.model._ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class CirceTests extends AnyFlatSpec with Matchers with EitherValues { diff --git a/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala b/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala index 330562825..46ebab91e 100644 --- a/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala +++ b/json/json4s/src/main/scala/sttp/client4/json4s/SttpJson4sApi.scala @@ -6,6 +6,8 @@ import sttp.client4.internal.Utf8 import sttp.client4.json._ import sttp.model._ import sttp.client4.ResponseAs.deserializeEitherOrThrow +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpJson4sApi { @@ -18,7 +20,8 @@ trait SttpJson4sApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: Manifest](implicit @@ -49,7 +52,7 @@ trait SttpJson4sApi { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: Manifest, B: Manifest](implicit @@ -58,8 +61,10 @@ trait SttpJson4sApi { ): ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - ResponseAs.deserializeCatchingExceptions(deserializeJson[E])(e, meta).fold(identity, HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + ResponseAs + .deserializeCatchingExceptions(deserializeJson[E])(e, meta) + .fold(identity, UnexpectedStatusCode(_, meta)) case de @ DeserializationException(_, _, _) => de } }.showAsJsonEither diff --git a/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala b/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala index bf9dd04fe..207cfffa0 100644 --- a/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala +++ b/json/json4s/src/test/scala/sttp/client4/Json4sTests.scala @@ -12,6 +12,7 @@ import scala.language.higherKinds import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class Json4sTests extends AnyFlatSpec with Matchers with EitherValues { implicit val serialization: Serialization.type = native.Serialization diff --git a/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala b/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala index 716aff161..85e25da12 100644 --- a/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala +++ b/json/jsoniter/src/main/scala/sttp/client4/jsoniter/SttpJsoniterJsonApi.scala @@ -1,7 +1,5 @@ package sttp.client4.jsoniter -import sttp.client4.DeserializationException -import sttp.client4.HttpError import sttp.client4.IsOption import sttp.client4.JsonInput import sttp.client4.ResponseAs @@ -12,6 +10,8 @@ import sttp.client4.asStringAlways import sttp.client4.internal.Utf8 import sttp.client4.json.RichResponseAs import sttp.model.MediaType +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpJsoniterJsonApi { import com.github.plokhotnyuk.jsoniter_scala.core._ @@ -22,7 +22,8 @@ trait SttpJsoniterJsonApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: JsonValueCodec: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -44,7 +45,7 @@ trait SttpJsoniterJsonApi { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[ @@ -54,8 +55,8 @@ trait SttpJsoniterJsonApi { asJson[B].mapLeft { (l: ResponseException[String]) => l match { case de @ DeserializationException(_, _, _) => de - case HttpError(e, meta) => - deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), UnexpectedStatusCode(_, meta)) } }.showAsJsonEither diff --git a/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala b/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala index ad5519685..73767f412 100644 --- a/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala +++ b/json/jsoniter/src/test/scala/sttp/client4/jsoniter/JsoniterJsonTests.scala @@ -10,6 +10,7 @@ import sttp.model._ import com.github.plokhotnyuk.jsoniter_scala.core._ import com.github.plokhotnyuk.jsoniter_scala.macros._ import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class JsoniterJsonTests extends AnyFlatSpec with Matchers with EitherValues { diff --git a/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala b/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala index fd0a50257..320f94d5d 100644 --- a/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala +++ b/json/play-json/src/main/scala/sttp/client4/playJson/SttpPlayJsonApi.scala @@ -8,6 +8,8 @@ import sttp.model.MediaType import scala.util.{Failure, Success, Try} import sttp.client4.ResponseAs.deserializeEitherWithErrorOrThrow +import sttp.client4.ResponseException.UnexpectedStatusCode +import sttp.client4.ResponseException.DeserializationException trait SttpPlayJsonApi { @@ -17,7 +19,8 @@ trait SttpPlayJsonApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: Reads: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -39,14 +42,14 @@ trait SttpPlayJsonApi { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: Reads: IsOption, B: Reads: IsOption]: ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), UnexpectedStatusCode(_, meta)) case de: DeserializationException => de } }.showAsJsonEither diff --git a/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala b/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala index b400b6b8f..a2100d8d6 100644 --- a/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala +++ b/json/play-json/src/test/scala/sttp/client4/PlayJsonTests.scala @@ -9,6 +9,7 @@ import sttp.model.StatusCode import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class PlayJsonTests extends AnyFlatSpec with Matchers with EitherValues { diff --git a/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala b/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala index a8b931fb0..e308aca19 100644 --- a/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala +++ b/json/spray-json/src/main/scala/sttp/client4/sprayJson/SttpSprayJsonApi.scala @@ -6,6 +6,8 @@ import sttp.client4._ import sttp.client4.json._ import sttp.model._ import sttp.client4.ResponseAs.deserializeEitherOrThrow +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpSprayJsonApi { @@ -15,7 +17,8 @@ trait SttpSprayJsonApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: JsonReader: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -37,14 +40,16 @@ trait SttpSprayJsonApi { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: JsonReader: IsOption, B: JsonReader: IsOption]: ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - ResponseAs.deserializeCatchingExceptions(deserializeJson[E])(e, meta).fold(identity, HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + ResponseAs + .deserializeCatchingExceptions(deserializeJson[E])(e, meta) + .fold(identity, UnexpectedStatusCode(_, meta)) case de: DeserializationException => de } }.showAsJsonEither diff --git a/json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala b/json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala index 30dcd5b9b..4d7cb2b10 100644 --- a/json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala +++ b/json/spray-json/src/test/scala/sttp/client4/sprayJson/SprayJsonTests.scala @@ -11,9 +11,9 @@ import sttp.client4.basicRequest import sttp.client4.PartialRequest import sttp.client4.StringBody import sttp.client4.Request -import sttp.client4.DeserializationException import spray.json.JsonParser.ParsingException import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class SprayJsonTests extends AnyFlatSpec with Matchers with EitherValues { import SprayJsonTests._ diff --git a/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala b/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala index 77b1dede2..97e05315b 100644 --- a/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala +++ b/json/tethys-json/src/main/scala/sttp/client4/tethysJson/SttpTethysApi.scala @@ -9,6 +9,8 @@ import tethys.readers.ReaderError import tethys.readers.tokens.TokenIteratorProducer import tethys.writers.tokens.TokenWriterProducer import sttp.client4.ResponseAs.deserializeEitherWithErrorOrThrow +import sttp.client4.ResponseException.UnexpectedStatusCode +import sttp.client4.ResponseException.DeserializationException trait SttpTethysApi { @@ -20,7 +22,8 @@ trait SttpTethysApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: JsonReader: IsOption](implicit diff --git a/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala b/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala index c9c519c78..bf5fa1e98 100644 --- a/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala +++ b/json/tethys-json/src/test/scala/sttp/client4/tethysJson/TethysTests.scala @@ -12,6 +12,7 @@ import tethys.readers.tokens.TokenIterator import tethys.readers.{FieldName, ReaderError} import tethys.{JsonReader, JsonWriter} import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException import scala.util.{Failure, Success, Try} diff --git a/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala b/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala index a6e8e0470..13f20ea28 100644 --- a/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala +++ b/json/upickle/src/main/scala/sttp/client4/upicklejson/SttpUpickleApi.scala @@ -5,6 +5,8 @@ import sttp.client4.internal.Utf8 import sttp.model.MediaType import sttp.client4.json._ import sttp.client4.ResponseAs.deserializeEitherWithErrorOrThrow +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpUpickleApi { val upickleApi: upickle.Api @@ -15,7 +17,8 @@ trait SttpUpickleApi { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: upickleApi.Reader: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -37,15 +40,15 @@ trait SttpUpickleApi { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: upickleApi.Reader: IsOption, B: upickleApi.Reader: IsOption] : ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), UnexpectedStatusCode(_, meta)) case de: DeserializationException => de } }.showAsJsonEither diff --git a/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala b/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala index ee1a77ea9..1469dfe82 100644 --- a/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala +++ b/json/upickle/src/test/scala/sttp/client4/upicklejson/UpickleTests.scala @@ -8,6 +8,7 @@ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import ujson.Obj import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class UpickleTests extends AnyFlatSpec with Matchers with EitherValues { "The upickle module" should "encode arbitrary bodies given an encoder" in { diff --git a/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala b/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala index d5d647fd8..8d198cda3 100644 --- a/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala +++ b/json/zio-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala @@ -1,7 +1,5 @@ package sttp.client4.ziojson -import sttp.client4.DeserializationException -import sttp.client4.HttpError import sttp.client4.IsOption import sttp.client4.JsonInput import sttp.client4.ResponseAs @@ -12,6 +10,8 @@ import sttp.client4.asStringAlways import sttp.client4.internal.Utf8 import sttp.client4.json.RichResponseAs import sttp.model.MediaType +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpZioJsonApi extends SttpZioJsonApiExtensions { import zio.json._ @@ -21,7 +21,8 @@ trait SttpZioJsonApi extends SttpZioJsonApiExtensions { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: JsonDecoder: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -43,14 +44,14 @@ trait SttpZioJsonApi extends SttpZioJsonApiExtensions { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: JsonDecoder: IsOption, B: JsonDecoder: IsOption]: ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), UnexpectedStatusCode(_, meta)) case de: DeserializationException => de } }.showAsJsonEither diff --git a/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala b/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala index ca50eaf5e..6a346be3c 100644 --- a/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala +++ b/json/zio-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala @@ -2,8 +2,6 @@ package sttp.client4.ziojson import sttp.capabilities.Effect import sttp.capabilities.zio.ZioStreams -import sttp.client4.DeserializationException -import sttp.client4.HttpError import sttp.client4.ResponseException import sttp.client4.StreamResponseAs import sttp.client4.asStreamWithMetadata @@ -11,6 +9,8 @@ import zio.Task import zio.ZIO import zio.json.JsonDecoder import zio.stream.ZPipeline +import sttp.client4.ResponseException.UnexpectedStatusCode +import sttp.client4.ResponseException.DeserializationException trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => def asJsonStream[B: JsonDecoder] @@ -21,7 +21,7 @@ trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => .map(Right(_)) .catchSome { case e: Exception => ZIO.left(DeserializationException("", e, meta)) } ).mapWithMetadata { - case (Left(s), meta) => Left(HttpError(s, meta)) + case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta)) case (Right(s), _) => s } } diff --git a/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala b/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala index ccd39e564..855cd0c5a 100644 --- a/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala +++ b/json/zio-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala @@ -10,6 +10,7 @@ import sttp.model._ import zio.Chunk import zio.json.ast.Json import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { diff --git a/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala b/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala index d5d647fd8..8d198cda3 100644 --- a/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala +++ b/json/zio1-json/src/main/scala/sttp/client4/ziojson/SttpZioJsonApi.scala @@ -1,7 +1,5 @@ package sttp.client4.ziojson -import sttp.client4.DeserializationException -import sttp.client4.HttpError import sttp.client4.IsOption import sttp.client4.JsonInput import sttp.client4.ResponseAs @@ -12,6 +10,8 @@ import sttp.client4.asStringAlways import sttp.client4.internal.Utf8 import sttp.client4.json.RichResponseAs import sttp.model.MediaType +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpZioJsonApi extends SttpZioJsonApiExtensions { import zio.json._ @@ -21,7 +21,8 @@ trait SttpZioJsonApi extends SttpZioJsonApiExtensions { /** If the response is successful (2xx), tries to deserialize the body from a string into JSON. Returns: * - `Right(b)` if the parsing was successful - * - `Left(HttpError(String))` if the response code was other than 2xx (deserialization is not attempted) + * - `Left(UnexpectedStatusCode(String))` if the response code was other than 2xx (deserialization is not + * attempted) * - `Left(DeserializationException)` if there's an error during deserialization */ def asJson[B: JsonDecoder: IsOption]: ResponseAs[Either[ResponseException[String], B]] = @@ -43,14 +44,14 @@ trait SttpZioJsonApi extends SttpZioJsonApiExtensions { /** Tries to deserialize the body from a string into JSON, using different deserializers depending on the status code. * Returns: * - `Right(B)` if the response was 2xx and parsing was successful - * - `Left(HttpError(E))` if the response was other than 2xx and parsing was successful + * - `Left(UnexpectedStatusCode(E))` if the response was other than 2xx and parsing was successful * - `Left(DeserializationException)` if there's an error during deserialization */ def asJsonEither[E: JsonDecoder: IsOption, B: JsonDecoder: IsOption]: ResponseAs[Either[ResponseException[E], B]] = asJson[B].mapLeft { (l: ResponseException[String]) => l match { - case HttpError(e, meta) => - deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), HttpError(_, meta)) + case UnexpectedStatusCode(e, meta) => + deserializeJson[E].apply(e).fold(DeserializationException(e, _, meta), UnexpectedStatusCode(_, meta)) case de: DeserializationException => de } }.showAsJsonEither diff --git a/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala b/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala index d2d67eeb6..b63a03838 100644 --- a/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala +++ b/json/zio1-json/src/main/scalajvm/sttp/client4/ziojson/SttpZioJsonApiExtensions.scala @@ -2,8 +2,6 @@ package sttp.client4.ziojson import sttp.capabilities.Effect import sttp.capabilities.zio.ZioStreams -import sttp.client4.DeserializationException -import sttp.client4.HttpError import sttp.client4.ResponseException import sttp.client4.StreamResponseAs import sttp.client4.asStreamWithMetadata @@ -12,6 +10,8 @@ import zio.ZIO import zio.blocking.Blocking import zio.json.JsonDecoder import zio.stream.ZTransducer +import sttp.client4.ResponseException.DeserializationException +import sttp.client4.ResponseException.UnexpectedStatusCode trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => def asJsonStream[B: JsonDecoder] @@ -22,7 +22,7 @@ trait SttpZioJsonApiExtensions { this: SttpZioJsonApi => .map(Right(_)) .catchSome { case e: Exception => ZIO.left(DeserializationException("", e, meta)) } ).mapWithMetadata { - case (Left(s), meta) => Left(HttpError(s, meta)) + case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta)) case (Right(s), _) => s } } diff --git a/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala b/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala index ccd39e564..855cd0c5a 100644 --- a/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala +++ b/json/zio1-json/src/test/scala/sttp/client4/ziojson/ZioJsonTests.scala @@ -10,6 +10,7 @@ import sttp.model._ import zio.Chunk import zio.json.ast.Json import sttp.client4.json.RunResponseAs +import sttp.client4.ResponseException.DeserializationException class ZioJsonTests extends AnyFlatSpec with Matchers with EitherValues { diff --git a/observability/opentelemetry-metrics-backend/src/test/scala/sttp/client4/opentelemetry/OpenTelemetryMetricsBackendTest.scala b/observability/opentelemetry-metrics-backend/src/test/scala/sttp/client4/opentelemetry/OpenTelemetryMetricsBackendTest.scala index 689ae212c..3e612c818 100644 --- a/observability/opentelemetry-metrics-backend/src/test/scala/sttp/client4/opentelemetry/OpenTelemetryMetricsBackendTest.scala +++ b/observability/opentelemetry-metrics-backend/src/test/scala/sttp/client4/opentelemetry/OpenTelemetryMetricsBackendTest.scala @@ -9,7 +9,7 @@ import org.scalatest.OptionValues import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import sttp.client4.testing.{ResponseStub, SyncBackendStub} -import sttp.client4.{asString, basicRequest, DeserializationException, SttpClientException, UriContext} +import sttp.client4.{asString, basicRequest, SttpClientException, UriContext} import sttp.model.{Header, StatusCode} import scala.collection.JavaConverters._ diff --git a/observability/prometheus-backend/src/test/scala/sttp/client4/prometheus/PrometheusBackendTest.scala b/observability/prometheus-backend/src/test/scala/sttp/client4/prometheus/PrometheusBackendTest.scala index 109b04577..3fd7a9650 100644 --- a/observability/prometheus-backend/src/test/scala/sttp/client4/prometheus/PrometheusBackendTest.scala +++ b/observability/prometheus-backend/src/test/scala/sttp/client4/prometheus/PrometheusBackendTest.scala @@ -423,7 +423,7 @@ class PrometheusBackendTest // given val backendStub = SyncBackendStub.whenAnyRequest.thenRespondF(_ => - throw new HttpError("boom", ResponseStub("", StatusCode.BadRequest)) + throw new ResponseException.UnexpectedStatusCode("boom", ResponseStub("", StatusCode.BadRequest)) ) import sttp.client4.prometheus.PrometheusBackend.{DefaultFailureCounterName, addMethodLabel}