Skip to content

Commit 0a2aaa9

Browse files
authored
Simplify ResponseException (#2397)
1 parent e992977 commit 0a2aaa9

File tree

56 files changed

+571
-462
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+571
-462
lines changed

core/src/main/scala/sttp/client4/Response.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ case class Response[+T](
3131
s"Response($body,$code,$statusText,${Headers.toStringSafe(headers)},$history,$request)"
3232
}
3333

34-
private[sttp] object Response {
34+
/** For testing, responses can be more conveniently created using [[ResponseStub]]. */
35+
object Response {
3536

3637
def apply[T](body: T, code: StatusCode, requestMetadata: RequestMetadata): Response[T] =
3738
Response(body, code, resolveStatusText(code), Nil, Nil, requestMetadata)

core/src/main/scala/sttp/client4/ResponseAs.scala

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package sttp.client4
22

3-
import sttp.capabilities.{Effect, Streams, WebSockets}
3+
import sttp.capabilities.Effect
4+
import sttp.capabilities.Streams
5+
import sttp.capabilities.WebSockets
6+
import sttp.client4.ResponseException.DeserializationException
7+
import sttp.client4.ResponseException.UnexpectedStatusCode
48
import sttp.client4.internal.SttpFile
59
import sttp.model.ResponseMetadata
610
import sttp.model.internal.Rfc3986
7-
import sttp.ws.{WebSocket, WebSocketFrame}
11+
import sttp.ws.WebSocket
12+
import sttp.ws.WebSocketFrame
813

914
import java.io.InputStream
1015
import scala.collection.immutable.Seq
11-
import scala.util.{Failure, Success, Try}
16+
import scala.util.Failure
17+
import scala.util.Success
18+
import scala.util.Try
1219

1320
/** Describes how the response body of a request should be handled. A number of `as<Type>` helper methods are available
1421
* as part of [[SttpApi]] and when importing `sttp.client4._`. These methods yield specific implementations of this
@@ -77,30 +84,30 @@ case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseA
7784
)
7885

7986
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
80-
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
81-
* yet an exception)
87+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if
88+
* `A` is not yet an exception)
8289
* - in case of `B`, returns the value directly
8390
*/
8491
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): ResponseAs[B] =
8592
mapWithMetadata { case (t, meta) =>
8693
(t: Either[A, B]) match {
8794
case Left(a: Exception) => throw a
88-
case Left(a) => throw HttpError(a, meta.code)
95+
case Left(a) => throw UnexpectedStatusCode(a, meta)
8996
case Right(b) => b
9097
}
9198
}
9299

93-
/** If the type to which the response body should be deserialized is an `Either[ResponseException[HE, DE], B]`, either
100+
/** If the type to which the response body should be deserialized is an `Either[ResponseException[HE], B]`, either
94101
* throws / returns a failed effect with the [[DeserializationException]], returns the deserialized body from the
95-
* [[HttpError]], or the deserialized successful body `B`.
102+
* [[UnexpectedStatusCode]], or the deserialized successful body `B`.
96103
*/
97-
def orFailDeserialization[HE, DE, B](implicit
98-
tIsEither: T <:< Either[ResponseException[HE, DE], B]
104+
def orFailDeserialization[HE, B](implicit
105+
tIsEither: T <:< Either[ResponseException[HE], B]
99106
): ResponseAs[Either[HE, B]] = map { t =>
100-
(t: Either[ResponseException[HE, DE], B]) match {
101-
case Left(HttpError(he, _)) => Left(he)
102-
case Left(d: DeserializationException[_]) => throw d
103-
case Right(b) => Right(b)
107+
(t: Either[ResponseException[HE], B]) match {
108+
case Left(UnexpectedStatusCode(he, _)) => Left(he)
109+
case Left(d: DeserializationException) => throw d
110+
case Right(b) => Right(b)
104111
}
105112
}
106113

@@ -109,22 +116,22 @@ case class ResponseAs[+T](delegate: GenericResponseAs[T, Any]) extends ResponseA
109116

110117
object ResponseAs {
111118

112-
/** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using
113-
* the given function, catching any exceptions and representing them as [[DeserializationException]] s.
119+
/** Returns a function, which maps `Left` values to [[UnexpectedStatusCode]] s, and attempts to deserialize `Right`
120+
* values using the given function, catching any exceptions and representing them as [[DeserializationException]] s.
114121
*/
115122
def deserializeRightCatchingExceptions[T](
116123
doDeserialize: String => T
117-
): (Either[String, String], ResponseMetadata) => Either[ResponseException[String, Exception], T] = {
118-
case (Left(s), meta) => Left(HttpError(s, meta.code))
119-
case (Right(s), _) => deserializeCatchingExceptions(doDeserialize)(s)
124+
): (Either[String, String], ResponseMetadata) => Either[ResponseException[String], T] = {
125+
case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta))
126+
case (Right(s), meta) => deserializeCatchingExceptions(doDeserialize)(s, meta)
120127
}
121128

122129
/** Returns a function, which attempts to deserialize `Right` values using the given function, catching any exceptions
123130
* and representing them as [[DeserializationException]] s.
124131
*/
125132
def deserializeCatchingExceptions[T](
126133
doDeserialize: String => T
127-
): String => Either[DeserializationException[Exception], T] =
134+
): (String, ResponseMetadata) => Either[DeserializationException, T] =
128135
deserializeWithError((s: String) =>
129136
Try(doDeserialize(s)) match {
130137
case Failure(e: Exception) => Left(e)
@@ -133,63 +140,65 @@ object ResponseAs {
133140
}
134141
)
135142

136-
/** Returns a function, which maps `Left` values to [[HttpError]] s, and attempts to deserialize `Right` values using
137-
* the given function.
143+
/** Returns a function, which maps `Left` values to [[UnexpectedStatusCode]] s, and attempts to deserialize `Right`
144+
* values using the given function.
138145
*/
139-
def deserializeRightWithError[E: ShowError, T](
140-
doDeserialize: String => Either[E, T]
141-
): (Either[String, String], ResponseMetadata) => Either[ResponseException[String, E], T] = {
142-
case (Left(s), meta) => Left(HttpError(s, meta.code))
143-
case (Right(s), _) => deserializeWithError(doDeserialize)(implicitly[ShowError[E]])(s)
146+
def deserializeRightWithError[T](
147+
doDeserialize: String => Either[Exception, T]
148+
): (Either[String, String], ResponseMetadata) => Either[ResponseException[String], T] = {
149+
case (Left(s), meta) => Left(UnexpectedStatusCode(s, meta))
150+
case (Right(s), meta) => deserializeWithError(doDeserialize)(s, meta)
144151
}
145152

146153
/** Returns a function, which keeps `Left` unchanged, and attempts to deserialize `Right` values using the given
147154
* function. If deserialization fails, an exception is thrown
148155
*/
149-
def deserializeRightOrThrow[E: ShowError, T](
150-
doDeserialize: String => Either[E, T]
151-
): Either[String, String] => Either[String, T] = {
152-
case Left(s) => Left(s)
153-
case Right(s) => Right(deserializeOrThrow(doDeserialize)(implicitly[ShowError[E]])(s))
156+
def deserializeRightOrThrow[T](
157+
doDeserialize: String => Either[Exception, T]
158+
): (Either[String, String], ResponseMetadata) => Either[String, T] = {
159+
case (Left(s), _) => Left(s)
160+
case (Right(s), m) => Right(deserializeOrThrow(doDeserialize)(s, m))
154161
}
155162

156163
/** Converts a deserialization function, which returns errors of type `E`, into a function where errors are wrapped
157164
* using [[DeserializationException]].
158165
*/
159-
def deserializeWithError[E: ShowError, T](
160-
doDeserialize: String => Either[E, T]
161-
): String => Either[DeserializationException[E], T] =
162-
s =>
166+
def deserializeWithError[T](
167+
doDeserialize: String => Either[Exception, T]
168+
): (String, ResponseMetadata) => Either[DeserializationException, T] =
169+
(s, meta) =>
163170
doDeserialize(s) match {
164-
case Left(e) => Left(DeserializationException(s, e))
171+
case Left(e) => Left(DeserializationException(s, e, meta))
165172
case Right(b) => Right(b)
166173
}
167174

168175
/** Converts a deserialization function, which returns errors of type `E`, into a function where errors are thrown as
169176
* exceptions, and results are returned unwrapped.
170177
*/
171-
def deserializeOrThrow[E: ShowError, T](doDeserialize: String => Either[E, T]): String => T =
172-
s =>
178+
def deserializeOrThrow[T](
179+
doDeserialize: String => Either[Exception, T]
180+
): (String, ResponseMetadata) => T =
181+
(s, meta) =>
173182
doDeserialize(s) match {
174-
case Left(e) => throw DeserializationException(s, e)
183+
case Left(e) => throw DeserializationException(s, e, meta)
175184
case Right(b) => b
176185
}
177186

178187
/** Converts deserialization functions, which both return errors of type `E`, into a function where errors are thrown
179-
* as exceptions, and results are parsed using either of the functions, depending if the response was successfull, or
188+
* as exceptions, and results are parsed using either of the functions, depending if the response was successful, or
180189
* not.
181190
*/
182-
def deserializeEitherWithErrorOrThrow[E: ShowError, T, T2](
183-
doDeserializeHttpError: String => Either[E, T],
184-
doDeserializeHttpSuccess: String => Either[E, T2]
191+
def deserializeEitherWithErrorOrThrow[T, T2](
192+
doDeserializeHttpError: String => Either[Exception, T],
193+
doDeserializeHttpSuccess: String => Either[Exception, T2]
185194
): (String, ResponseMetadata) => Either[T, T2] =
186195
(s, m) =>
187-
if (m.isSuccess) Right(deserializeOrThrow(doDeserializeHttpSuccess).apply(s))
188-
else Left(deserializeOrThrow(doDeserializeHttpError).apply(s))
196+
if (m.isSuccess) Right(deserializeOrThrow(doDeserializeHttpSuccess).apply(s, m))
197+
else Left(deserializeOrThrow(doDeserializeHttpError).apply(s, m))
189198

190199
/** Converts deserialization functions, which both throw exceptions upon errors, into a function where errors still
191200
* thrown as exceptions, and results are parsed using either of the functions, depending if the response was
192-
* successfull, or not.
201+
* successful, or not.
193202
*/
194203
def deserializeEitherOrThrow[T, T2](
195204
doDeserializeHttpError: String => T,
@@ -226,15 +235,15 @@ case class StreamResponseAs[+T, S](delegate: GenericResponseAs[T, S]) extends Re
226235
StreamResponseAs(delegate.mapWithMetadata(f))
227236

228237
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
229-
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
230-
* yet an exception)
238+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if
239+
* `A` is not yet an exception)
231240
* - in case of `B`, returns the value directly
232241
*/
233242
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): StreamResponseAs[B, S] =
234243
mapWithMetadata { case (t, meta) =>
235244
(t: Either[A, B]) match {
236245
case Left(a: Exception) => throw a
237-
case Left(a) => throw HttpError(a, meta.code)
246+
case Left(a) => throw UnexpectedStatusCode(a, meta)
238247
case Right(b) => b
239248
}
240249
}
@@ -267,15 +276,15 @@ case class WebSocketResponseAs[F[_], +T](delegate: GenericResponseAs[T, Effect[F
267276
WebSocketResponseAs(delegate.mapWithMetadata(f))
268277

269278
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
270-
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
271-
* yet an exception)
279+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if
280+
* `A` is not yet an exception)
272281
* - in case of `B`, returns the value directly
273282
*/
274283
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketResponseAs[F, B] =
275284
mapWithMetadata { case (t, meta) =>
276285
(t: Either[A, B]) match {
277286
case Left(a: Exception) => throw a
278-
case Left(a) => throw HttpError(a, meta.code)
287+
case Left(a) => throw UnexpectedStatusCode(a, meta)
279288
case Right(b) => b
280289
}
281290
}
@@ -308,15 +317,15 @@ case class WebSocketStreamResponseAs[+T, S](delegate: GenericResponseAs[T, S wit
308317
WebSocketStreamResponseAs[T2, S](delegate.mapWithMetadata(f))
309318

310319
/** If the type to which the response body should be deserialized is an `Either[A, B]`:
311-
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[HttpError]] if `A` is not
312-
* yet an exception)
320+
* - in case of `A`, throws as an exception / returns a failed effect (wrapped with an [[UnexpectedStatusCode]] if
321+
* `A` is not yet an exception)
313322
* - in case of `B`, returns the value directly
314323
*/
315324
def orFail[A, B](implicit tIsEither: T <:< Either[A, B]): WebSocketStreamResponseAs[B, S] =
316325
mapWithMetadata { case (t, meta) =>
317326
(t: Either[A, B]) match {
318327
case Left(a: Exception) => throw a
319-
case Left(a) => throw HttpError(a, meta.code)
328+
case Left(a) => throw UnexpectedStatusCode(a, meta)
320329
case Right(b) => b
321330
}
322331
}
Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,56 @@
11
package sttp.client4
22

3-
import sttp.model.StatusCode
4-
53
import scala.annotation.tailrec
4+
import sttp.model.ResponseMetadata
65

7-
/** Used to represent errors, that might occur when handling the response body. Typically, this type is used as the
8-
* left-side of a top-level either (where the right-side represents a successfull request and deserialization).
6+
/** Used to represent errors, that might occur when handling the response body, that was otherwise received
7+
* successfully.
8+
*
9+
* A response exception can itself be one of two cases:
10+
* - [[ResponseException.UnexpectedStatusCode]], when the response code is other than 2xx (or whatever is considered
11+
* "success" by the response handling description); the body is deserialized to `HE`
12+
* - [[ResponseException.DeserializationException]], when there's an error during deserialization (this includes
13+
* deserialization exceptions of both the success and error branches)
914
*
10-
* A response exception can itself either be one of two cases:
11-
* - a [[HttpError]], when the response code is other than 2xx (or whatever is considered "success" by the response
12-
* handling description); the body is deserialized to `HE`
13-
* - a [[DeserializationException]], when there's an error during deserialization (this might include both
14-
* deserialization exceptions of the success and error branches)
15+
* This type is often used as the left-side of a top-level either (where the right-side represents a successful request
16+
* and deserialization). When thrown/returned when sending a request (e.g. in `...OrFailed` response handling
17+
* descriptions), will be additionally wrapped with a [[SttpClientException.ResponseHandlingException]].
1518
*
1619
* @tparam HE
1720
* The type of the body to which the response is deserialized, when the response code is different than success
1821
* (typically 2xx status code).
19-
* @tparam DE
20-
* A deserialization-library-specific error type, describing the deserialization error in more detail.
21-
*/
22-
sealed abstract class ResponseException[+HE, +DE](error: String, cause: Option[Throwable])
23-
extends Exception(error, cause.orNull)
24-
25-
/** Represents an http error, where the response was received successfully, but the status code is other than the
26-
* expected one (typically other than 2xx).
27-
*
28-
* @tparam HE
29-
* The type of the body to which the error response is deserialized.
30-
*/
31-
case class HttpError[+HE](body: HE, statusCode: StatusCode)
32-
extends ResponseException[HE, Nothing](s"statusCode: $statusCode, response: $body", None)
33-
34-
/** Represents an error that occured during deserialization of `body`.
35-
*
36-
* @tparam DE
37-
* A deserialization-library-specific error type, describing the deserialization error in more detail.
3822
*/
39-
case class DeserializationException[+DE: ShowError](body: String, error: DE)
40-
extends ResponseException[Nothing, DE](
41-
implicitly[ShowError[DE]].show(error),
42-
if (error.isInstanceOf[Throwable]) Some(error.asInstanceOf[Throwable]) else None
43-
)
44-
45-
object HttpError {
46-
@tailrec def find(exception: Throwable): Option[HttpError[_]] =
23+
sealed abstract class ResponseException[+HE](
24+
error: String,
25+
cause: Option[Throwable],
26+
val response: ResponseMetadata
27+
) extends Exception(error, cause.orNull)
28+
29+
object ResponseException {
30+
31+
/** Represents an error, where the response was received successfully, but the status code is other than the expected
32+
* one (typically other than 2xx).
33+
*
34+
* @tparam HE
35+
* The type of the body to which the error response is deserialized.
36+
*/
37+
case class UnexpectedStatusCode[+HE](body: HE, override val response: ResponseMetadata)
38+
extends ResponseException[HE](s"statusCode: ${response.code}, response: $body", None, response)
39+
40+
/** Represents an error that occurred during deserialization of `body`. */
41+
case class DeserializationException(body: String, cause: Exception, override val response: ResponseMetadata)
42+
extends ResponseException[Nothing](
43+
cause.getMessage(),
44+
Some(cause),
45+
response
46+
)
47+
48+
//
49+
50+
@tailrec def find(exception: Throwable): Option[ResponseException[_]] =
4751
Option(exception) match {
48-
case Some(error: HttpError[_]) => Some(error)
49-
case Some(_) => find(exception.getCause)
50-
case None => Option.empty
52+
case Some(e: ResponseException[_]) => Some(e)
53+
case Some(_) => find(exception.getCause)
54+
case None => None
5155
}
5256
}
53-
54-
trait ShowError[-T] {
55-
def show(t: T): String
56-
}
57-
58-
object ShowError {
59-
implicit val showErrorMessageFromException: ShowError[Exception] = new ShowError[Exception] {
60-
override def show(t: Exception): String = t.getMessage
61-
}
62-
}

0 commit comments

Comments
 (0)