-
Notifications
You must be signed in to change notification settings - Fork 412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add tapErrorZIO and tapErrorCauseZIO to Route and Routes #2755
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## main #2755 +/- ##
==========================================
- Coverage 64.42% 64.34% -0.08%
==========================================
Files 148 148
Lines 8668 8678 +10
Branches 1589 1545 -44
==========================================
Hits 5584 5584
- Misses 3084 3094 +10 ☔ View full report in Codecov by Sentry. |
self match { | ||
case Provided(route, env) => Provided(route.tapErrorCauseZIO(f), env) | ||
case Augmented(route, aspect) => Augmented(route.tapErrorCauseZIO(f), aspect) | ||
case Handled(routePattern, handler, location) => Handled(routePattern, handler, location) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iirc, ensures handled that the typed error is handled. But a cause (defect) might still happen. So I think there should be a tap on the handler here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't seem to work as the Handled.handler
's failure type is Response
whereas f
's failure type is Err
which is not necessarily a Response
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can catch the Cause[Err]
, and if it contains an Err
, then you rethrow it. But if it's really a Cause[Nothing]
, i.e. pure defect, without an Err
inside it, then you can allow that to be caught by f
, since f
can handle those (Cause[Nothing]
is a subtype of Cause[Err]
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tjarvstrand This does need to be fixed, and then will be good to merge!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, busy week! :/
I've been struggling a bit with the error case of f
which, unless I'm mistaken, must be a Response
. Does something like this make sense?
/**
* Effectfully peeks at the unhandled failure cause of this Route.
*/
final def tapErrorCauseZIO[Err1 >: Err](
f: Cause[Err] => ZIO[Any, Err1, Any],
)(implicit trace: Trace): Route[Env, Err1] =
self match {
case Provided(route, env) => Provided(route.tapErrorCauseZIO(f), env)
case Augmented(route, aspect) => Augmented(route.tapErrorCauseZIO(f), aspect)
case Handled(routePattern, handler, location) =>
Handled(
routePattern,
handler.tapErrorCauseZIO {
case cause0: Cause[Err] => f(cause0).catchAllCause(cause => ZIO.fail(Response.fromCause(cause)))
case _ => ZIO.unit
},
location,
)
case Unhandled(rpm, handler, zippable, location) =>
Unhandled(rpm, handler.tapErrorCauseZIO(f), zippable, location)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tjarvstrand What I meant originally was we don't need pattern matching to determine if it's Cause[Nothing]
or not, according to John's previous comment.
You can catch the Cause[Err], and if it contains an Err, then you rethrow it. But if it's really a Cause[Nothing], i.e. pure defect, without an Err inside it, then you can allow that to be caught by f, since f can handle those (Cause[Nothing] is a subtype of Cause[Err]).
But now we want to let f
handle failure cases as well when Err =:= Response
, right?
I think there are two options:
-
Extract the possible failure with something likeEDIT: It seems irrelevant.Cause#failureOrCause
, and do runtime type check for the failure case using.isInstanceOf[Response]
. It's the simplest solution. -
Require an implicit
zio.Tag[Err]
orscala.reflect.ClassTag[Err]
and check its runtime class. -
Add an implicit evidence like this:
sealed trait IsResponse[+A] { def isResponse: Boolean } object IsResponse { implicit val response: IsResponse[Response] = new IsResponse[Response] { isResponse = true } // to avoid unnecessary allocation private val otherInstance: IsResponse[Nothing] = new IsResponse[Nothing] { isResponse = false } implicit def other: IsResponse[A] = otherInstance }
The name
IsResponse
might cause confusion withHandler.IsRequest
, which has the similar purpose but it's for compile time check only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, Handled.handler
is a handler that produces another handler (code), so checking handler
's failure won't achieve what you want. You need handler.map(_.tapErrorCauseZIO(...))
to catch the Response
failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for your comments!
If I understand it correctly, both alternative 2 and 3 would require a call to asInstanceOf
, since neither of them provide a type level proof that Err <: Response
. Is that correct?
By the way, Handled.handler is a handler that produces another handler (code), so checking handler's failure won't achieve what you want. You need handler.map(_.tapErrorCauseZIO(...)) to catch the Response failure.
Aha, I missed that! Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternative 2 sadly doesn't work since ClassTag
is invariant: Covariant type Err occurs in invariant position in type ClassTag[Err] of value ct
.
I gave alternative 3 a shot instead, but I had to make it contravariant rather than covariant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't directly summon ClassTag[Err]
because it's invariant. Instead, introduce a new type variable like Err1
.
Check out ZIO#refinedToOrDie for an example:
def refineToOrDie[E1 <: E: ClassTag](implicit ev: CanFail[E], trace: Trace): ZIO[R, E1, A] =
self.refineOrDie { case e: E1 => e }
006b320
to
1194943
Compare
1f6c5e0
to
5589573
Compare
Handled( | ||
routePattern, | ||
handler.map(_.tapErrorCauseZIO { | ||
case err: Fail[_] if ev.isResponse => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ev.IsResponse == false
we don't need handler.map(...)
at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True!
routePattern, | ||
handler.map(_.tapErrorCauseZIO { | ||
case cause0 if ev.isResponse => | ||
f(cause0.asInstanceOf[Cause[Err]]).catchAllCause(cause => ZIO.fail(Response.fromCause(cause))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the cause is not a failure, it can be passed to f
even if Err =!= Response
as John pointed out in the previous comment I quoted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah, sorry I misread that one!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here about the .catchAllCause
@@ -489,4 +537,16 @@ object Route { | |||
} | |||
} | |||
|
|||
sealed trait IsResponseCauseHandler[-A] { def isResponse: Boolean } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The most type evidences starting with Is
tend provide the implicit instance only and if only the type.
As it always provides the implicit value, maybe we need another name to avoid confusion.
Possible alternatives:
DetermineResponse
CheckResponse
ResponseTypeChecker
- ...
I think CauseHandler
is somewhat misleading as this type is not directly related to handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Changing it to CheckResponse
Thanks for the feedback! |
cd742b1
to
933ec35
Compare
routePattern, | ||
if (ev.isResponse) { | ||
handler.map(_.tapErrorCauseZIO { | ||
case err: Fail[_] if ev.isResponse => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now we don't need this pattern guard :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed!
if (ev.isResponse) { | ||
handler.map(_.tapErrorCauseZIO { | ||
case err: Fail[_] if ev.isResponse => | ||
f(err.value.asInstanceOf[Err]).catchAllCause(cause => ZIO.fail(Response.fromCause(cause))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I doubt this .catchAllCause(cause => ZIO.fail(Response.fromCause(cause)))
is necessary, because it may cause unexpected behavior when there is a global error handler, say catching all defects and produces a formatted error response.
As we're sure that Err =:= Response
, just '.asInstance[...]' can be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we do need the catchAllCause
since we don't know that Err1 <: Response
routePattern, | ||
handler.map(_.tapErrorCauseZIO { | ||
case cause0 if ev.isResponse => | ||
f(cause0.asInstanceOf[Cause[Err]]).catchAllCause(cause => ZIO.fail(Response.fromCause(cause))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here about the .catchAllCause
case _: Fail[_] => | ||
ZIO.unit | ||
case cause0 => | ||
f(cause0.asInstanceOf[Cause[Nothing]]).catchAllCause(cause => ZIO.fail(Response.fromCause(cause))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here about the .catchAllCause
.
@tjarvstrand Could you handle the review comments? I'd like to get this in for 3.0 |
5a700da
to
c45935a
Compare
The tests seem to fail on something unrelated to my changes. Not sure if it's a fluke but I don't seem to be allowed to restart the workflow. |
I updated your branch. Let's see if it builds |
case Handled(routePattern, handler, location) => | ||
Handled( | ||
routePattern, | ||
if (ev.isResponse) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is always true. Handled
has a fixed error type of Nothing
and will pick up the implicit val response: CheckResponse[Response]
Handled Routes are by definition routes without error. This should just returned the route without any modification.
The becomes clear when substituting the types. Because f is Nothing => ZIO[Any, Err1, Any]
Handled( | ||
routePattern, | ||
handler.map(_.tapErrorCauseZIO { | ||
case cause0 if ev.isResponse => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ev.isResponse
is always true. Also the concrete type of f
here is Cause[Nothing] => ZIO[Any, Err1, Any]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having f
as Cause[Nothing] => ZIO[Any, Err1, Any]
doesn't seem to work as it makes the call to handler.tapErrorCauseZIO
in the Unhandled
-case sad.
Please add some tests. For example adding a log output and use the test logger to find the message you logged. Or use a promise. Or whatever comes to your mind :) |
@tjarvstrand could you give this PR the finishing touch? :) |
Looking into writing some tests ATM. In the meantime, I think I'm lacking the context needed to make wise decisions on my own here 😄 Just to make sure I don't misunderstand, you are saying that I should basically just return the same
EDIT: I think I figured most of it out. Everything became clearer once my mind got back into "scala mode". Please have another look. However, I noticed that the handler of an |
64337ba
to
ed54097
Compare
ed54097
to
255f105
Compare
Useful for things like error logging or reporting to external services such as Sentry.