Skip to content
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

Check if ReadableStream is locked in all exit cases #5103

Merged
merged 3 commits into from Aug 23, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 15 additions & 14 deletions dom-core/src/main/scala/org/http4s/dom/package.scala
Expand Up @@ -60,19 +60,20 @@ package object dom {
}

private[dom] def closeReadableStream[F[_], A](rs: ReadableStream[A], exitCase: Resource.ExitCase)(
implicit F: Async[F]): F[Unit] = exitCase match {
case Resource.ExitCase.Succeeded =>
F.fromPromise {
F.delay {
// Best guess: Firefox internally locks a ReadableStream after it is "drained"
// This checks if the stream is locked before canceling it to avoid an error
if (!rs.locked) rs.cancel(null) else scalajs.js.Promise.resolve[Unit](())
}
}.void
case Resource.ExitCase.Errored(ex) =>
F.fromPromise(F.delay(rs.cancel(ex.getMessage()))).void
case Resource.ExitCase.Canceled =>
F.fromPromise(F.delay(rs.cancel(null))).void
}
implicit F: Async[F]): F[Unit] = F.fromPromise {
F.delay {
// Best guess: Firefox internally locks a ReadableStream after it is "drained"
// This checks if the stream is locked before canceling it to avoid an error
if (!rs.locked) exitCase match {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!rs.locked) exitCase match {
if (rs.locked) {
scalajs.js.Promise.resolve[Unit](())
} else {
val message: Option[String] = exitCase match {
case Resrouce.ExitCase.Errored(ex) =>
Option(ex.getLocalizedMessage())
case _ =>
None
}
rs.cancel(message.getOrElse(null))
}

I'm good with what you have, but this separates the creation of the message from the cancellation of the ReadableStream and flips the boolean order. Some people feel that if (!condition) is non-ideal because it can people will often miss the negation when skimming the code. I've not strong feelings here though, just a thought.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid points. IMO my two-liner comment draws attention to the conditional which really shouldn't be necessary (!rs.locked is the happy path), Firefox is doing something weird here that isn't described in the spec AFAICT. And I think rs.cancel() is simple enough that seperating out the cases in a single expression makes it easy to follow the control flow here. All very subjective of course :)

But your ex.getLocalizedMessage() is a good touch. Except it reminds me, for many exceptions isn't the message null? Maybe we should do ex.toString() here so at least the exception class gets into the reason, if there is no message?

Let's not kid ourselves, this is an ugly bit of code that's not helped by the fact the spec doesn't describe what kind of argument the cancel method takes. Note to self: fix the null thing in scala-js-dom.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of .toString on the exception myself. Yes, the message can be null, but getMessage and getLocalizedMessage are the canonical interfaces to describe a Throwable error. I'd only want to bypass that if the message was null. If we really wanted to go down the path of trying to get the class name out if the mesage is null, there are methods that one can take to do so.

 def throwableToDescription(t: Throwable): String =
    Option(t.getLocalizedMessage).getOrElse(
     s"Encountered error of type ${t.getClass.getName} with null message."
    )

That said, I'm fine with any of the encodings you proposed. This is highly subjective territory so whatever you choose you've my 👍.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, so much bike shed! Localized message it is. Now I just hope someone actually sees it and finds it useful to make it all worth our trouble 😆 its unclear in the spec if we are not merely filling a blackhole.

A human-readable reason for the cancellation. The underlying source may or may not use it.

case Resource.ExitCase.Succeeded =>
rs.cancel(null)
case Resource.ExitCase.Errored(ex) =>
rs.cancel(ex.getLocalizedMessage())
case Resource.ExitCase.Canceled =>
rs.cancel(null)
}
else scalajs.js.Promise.resolve[Unit](())
}
}.void

}