Skip to content

Commit ccbf044

Browse files
authored
Merge pull request #267 from http4s/pr/fix-ws-connect-error
Fix WebSocket connect error handling
2 parents 5d05935 + a54e0d4 commit ccbf044

File tree

2 files changed

+32
-25
lines changed

2 files changed

+32
-25
lines changed

dom/src/main/scala/org/http4s/dom/WebSocketClient.scala

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import org.scalajs.dom.WebSocket
3939
import org.typelevel.ci._
4040
import scodec.bits.ByteVector
4141

42+
import java.io.IOException
4243
import scala.scalajs.js
4344
import scala.scalajs.js.JSConverters._
4445

@@ -50,7 +51,6 @@ object WebSocketClient {
5051
dispatcher <- Dispatcher.sequential[F]
5152
messages <- Queue.unbounded[F, Option[MessageEvent]].toResource
5253
semaphore <- Semaphore[F](1).toResource
53-
error <- F.deferred[Throwable].toResource
5454
close <- F.deferred[CloseEvent].toResource
5555
ws <- Resource.makeCase {
5656
F.async_[WebSocket] { cb =>
@@ -67,15 +67,23 @@ object WebSocketClient {
6767
ws.binaryType = "arraybuffer" // the default is blob
6868

6969
ws.onopen = { _ =>
70-
ws.onerror = // replace the error handler
71-
e => dispatcher.unsafeRunAndForget(error.complete(js.JavaScriptException(e)))
70+
ws.onmessage = // setup message handler
71+
e => dispatcher.unsafeRunAndForget(messages.offer(Some(e)))
72+
73+
ws.onclose = // replace the close handler
74+
e => dispatcher.unsafeRunAndForget(messages.offer(None) *> close.complete(e))
75+
76+
// no explicit error handler. according to spec:
77+
// 1. an error event is *always* followed by a close event and
78+
// 2. an error event doesn't carry any useful information *by design*
79+
7280
cb(Right(ws))
7381
}
7482

75-
ws.onerror = e => cb(Left(js.JavaScriptException(e)))
76-
ws.onmessage = e => dispatcher.unsafeRunAndForget(messages.offer(Some(e)))
77-
ws.onclose =
78-
e => dispatcher.unsafeRunAndForget(messages.offer(None) *> close.complete(e))
83+
// a close at this stage can only be an error
84+
// following spec we cannot get any detail about the error
85+
// https://websockets.spec.whatwg.org/#eventdef-websocket-error
86+
ws.onclose = _ => cb(Left(new IOException("Connection failed")))
7987
}
8088
} {
8189
case (ws, exitCase) =>
@@ -117,33 +125,27 @@ object WebSocketClient {
117125
def closeFrame: DeferredSource[F, WSFrame.Close] =
118126
(close: DeferredSource[F, CloseEvent]).map(e => WSFrame.Close(e.code, e.reason))
119127

120-
def receive: F[Option[WSDataFrame]] = semaphore
121-
.permit
122-
.surround(OptionT(messages.take).map(decodeMessage).value)
123-
.race(error.get.flatMap(F.raiseError[Option[WSDataFrame]]))
124-
.map(_.merge)
128+
def receive: F[Option[WSDataFrame]] =
129+
semaphore.permit.surround(OptionT(messages.take).map(decodeMessage).value)
125130

126131
override def receiveStream: Stream[F, WSDataFrame] =
127-
Stream
128-
.resource(semaphore.permit)
129-
.flatMap(_ => Stream.fromQueueNoneTerminated(messages))
130-
.map(decodeMessage)
131-
.concurrently(Stream.exec(error.get.flatMap(F.raiseError)))
132+
Stream.resource(semaphore.permit) >>
133+
Stream.fromQueueNoneTerminated(messages).map(decodeMessage)
132134

133135
private def decodeMessage(e: MessageEvent): WSDataFrame =
134136
e.data match {
135137
case s: String => WSFrame.Text(s)
136138
case b: js.typedarray.ArrayBuffer =>
137139
WSFrame.Binary(ByteVector.fromJSArrayBuffer(b))
138140
case _ => // this should never happen
139-
throw new RuntimeException
141+
throw new AssertionError
140142
}
141143

142144
override def sendText(text: String): F[Unit] =
143-
errorOr(F.delay(ws.send(text)))
145+
F.delay(ws.send(text))
144146

145147
override def sendBinary(bytes: ByteVector): F[Unit] =
146-
errorOr(F.delay(ws.send(bytes.toJSArrayBuffer)))
148+
F.delay(ws.send(bytes.toJSArrayBuffer))
147149

148150
def send(wsf: WSDataFrame): F[Unit] =
149151
wsf match {
@@ -153,11 +155,6 @@ object WebSocketClient {
153155
F.raiseError(new IllegalArgumentException("DataFrames cannot be fragmented"))
154156
}
155157

156-
private def errorOr(fu: F[Unit]): F[Unit] = error.tryGet.flatMap {
157-
case Some(error) => F.raiseError(error)
158-
case None => fu
159-
}
160-
161158
def sendMany[G[_]: Foldable, A <: WSDataFrame](wsfs: G[A]): F[Unit] =
162159
wsfs.foldMapM(send(_))
163160

testsBrowser/src/test/scala/org/http4s/dom/WebSocketSuite.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import org.http4s.client.websocket.WSRequest
2424
import org.http4s.dom.BuildInfo.fileServicePort
2525
import scodec.bits.ByteVector
2626

27+
import java.io.IOException
28+
2729
class WebSocketSuite extends CatsEffectSuite {
2830

2931
test("send and receive frames") {
@@ -46,4 +48,12 @@ class WebSocketSuite extends CatsEffectSuite {
4648
)
4749
}
4850

51+
test("Raises IOException on connect error") {
52+
WebSocketClient[IO]
53+
.connectHighLevel(
54+
WSRequest(Uri.fromString(s"ws://localhost:${fileServicePort}/not-ws").toOption.get))
55+
.use_
56+
.intercept[IOException]
57+
}
58+
4959
}

0 commit comments

Comments
 (0)