Skip to content

Commit

Permalink
Merge pull request typelevel#2655 from armanbilge/topic/io-exceptions
Browse files Browse the repository at this point in the history
Improve IO exceptions on Node.js
  • Loading branch information
mpilquist authored Oct 4, 2021
2 parents 4403401 + c0de14b commit b2a90a1
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 77 deletions.
26 changes: 23 additions & 3 deletions io/js/src/main/scala/fs2/io/IOException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,40 @@ package fs2.io

import fs2.io.file.FileSystemException
import fs2.io.net.SocketException
import fs2.io.net.SocketTimeoutException
import fs2.io.net.UnknownHostException
import fs2.io.net.tls.SSLException

import scala.scalajs.js
import fs2.io.net.SocketTimeoutException
import scala.util.control.NoStackTrace

private class JavaScriptIOException(message: String, cause: js.JavaScriptException)
extends IOException(message, cause)
with NoStackTrace

object IOException {
private[io] def unapply(cause: js.JavaScriptException): Option[IOException] =
SocketException
InterruptedIOException
.unapply(cause)
.orElse(SocketTimeoutException.unapply(cause))
.orElse(SocketException.unapply(cause))
.orElse(SSLException.unapply(cause))
.orElse(FileSystemException.unapply(cause))
.orElse(UnknownHostException.unapply(cause))
.orElse {
cause.exception match {
case error: js.Error if error.message.contains("EPIPE") =>
Some(new JavaScriptIOException("Broken pipe", cause))
case _ => None
}
}
}

class InterruptedIOException(message: String = null, cause: Throwable = null)
extends IOException(message, cause)

object InterruptedIOException {
private[io] def unapply(cause: js.JavaScriptException): Option[InterruptedIOException] =
SocketTimeoutException.unapply(cause)
}

class ClosedChannelException extends IOException
55 changes: 30 additions & 25 deletions io/js/src/main/scala/fs2/io/file/FileSystemException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,42 +40,45 @@ object FileSystemException {

class AccessDeniedException(message: String = null, cause: Throwable = null)
extends FileSystemException(message, cause)
private class JavaScriptAccessDeniedException(cause: js.JavaScriptException)
extends AccessDeniedException(cause = cause)
private class JavaScriptAccessDeniedException(file: String, cause: js.JavaScriptException)
extends AccessDeniedException(file, cause)
with NoStackTrace
object AccessDeniedException {
private[file] def unapply(cause: js.JavaScriptException): Option[AccessDeniedException] =
cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("EACCES") =>
Some(new JavaScriptAccessDeniedException(cause))
cause.exception match {
case error: js.Error if error.message.contains("EACCES") =>
val file = error.message.split('\'').toList.lastOption.getOrElse("<unknown>")
Some(new JavaScriptAccessDeniedException(file, cause))
case _ => None
}
}

class DirectoryNotEmptyException(message: String = null, cause: Throwable = null)
extends FileSystemException(message, cause)
private class JavaScriptDirectoryNotEmptyException(cause: js.JavaScriptException)
extends DirectoryNotEmptyException(cause = cause)
private class JavaScriptDirectoryNotEmptyException(file: String, cause: js.JavaScriptException)
extends DirectoryNotEmptyException(file, cause)
with NoStackTrace
object DirectoryNotEmptyException {
private[file] def unapply(cause: js.JavaScriptException): Option[DirectoryNotEmptyException] =
cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("ENOTEMPTY") =>
Some(new JavaScriptDirectoryNotEmptyException(cause))
cause.exception match {
case error: js.Error if error.message.contains("ENOTEMPTY") =>
val file = error.message.split('\'').toList.lastOption.getOrElse("<unknown>")
Some(new JavaScriptDirectoryNotEmptyException(file, cause))
case _ => None
}
}

class FileAlreadyExistsException(message: String = null, cause: Throwable = null)
extends FileSystemException(message, cause)
private class JavaScriptFileAlreadyExistsException(cause: js.JavaScriptException)
extends FileAlreadyExistsException(cause = cause)
private class JavaScriptFileAlreadyExistsException(file: String, cause: js.JavaScriptException)
extends FileAlreadyExistsException(file, cause)
with NoStackTrace
object FileAlreadyExistsException {
private[file] def unapply(cause: js.JavaScriptException): Option[FileAlreadyExistsException] =
cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("EEXIST") =>
Some(new JavaScriptFileAlreadyExistsException(cause))
cause.exception match {
case error: js.Error if error.message.contains("EEXIST") =>
val file = error.message.split('\'').toList.lastOption.getOrElse("<unknown>")
Some(new JavaScriptFileAlreadyExistsException(file, cause))
case _ => None
}
}
Expand All @@ -84,28 +87,30 @@ class FileSystemLoopException(file: String) extends FileSystemException(file)

class NoSuchFileException(message: String = null, cause: Throwable = null)
extends FileSystemException(message, cause)
private class JavaScriptNoSuchFileException(cause: js.JavaScriptException)
extends NoSuchFileException(cause = cause)
private class JavaScriptNoSuchFileException(file: String, cause: js.JavaScriptException)
extends NoSuchFileException(file, cause)
with NoStackTrace
object NoSuchFileException {
private[file] def unapply(cause: js.JavaScriptException): Option[NoSuchFileException] =
cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("ENOENT") =>
Some(new JavaScriptNoSuchFileException(cause))
cause.exception match {
case error: js.Error if error.message.contains("ENOENT") =>
val file = error.message.split('\'').toList.lastOption.getOrElse("<unknown>")
Some(new JavaScriptNoSuchFileException(file, cause))
case _ => None
}
}

class NotDirectoryException(message: String = null, cause: Throwable = null)
extends FileSystemException(message, cause)
private class JavaScriptNotDirectoryException(cause: js.JavaScriptException)
extends NotDirectoryException(cause = cause)
private class JavaScriptNotDirectoryException(file: String, cause: js.JavaScriptException)
extends NotDirectoryException(file, cause)
with NoStackTrace
object NotDirectoryException {
private[file] def unapply(cause: js.JavaScriptException): Option[NotDirectoryException] =
cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("ENOTDIR") =>
Some(new JavaScriptNotDirectoryException(cause))
cause.exception match {
case error: js.Error if error.message.contains("ENOTDIR") =>
val file = error.message.split('\'').toList.lastOption.getOrElse("<unknown>")
Some(new JavaScriptNotDirectoryException(file, cause))
case _ => None
}
}
81 changes: 52 additions & 29 deletions io/js/src/main/scala/fs2/io/net/NetException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,73 +23,96 @@ package fs2
package io
package net

import com.comcast.ip4s.Host
import com.comcast.ip4s.SocketAddress
import fs2.internal.jsdeps.node.osMod

import scala.scalajs.js
import scala.util.control.NoStackTrace
import scala.util.matching.Regex

class ProtocolException(message: String = null, cause: Throwable = null)
extends IOException(message, cause)

class SocketException(message: String = null, cause: Throwable = null)
extends IOException(message, cause)
private class JavaScriptSocketException(cause: js.JavaScriptException)
extends SocketException(cause = cause)
private class JavaScriptSocketException(message: String, cause: js.JavaScriptException)
extends SocketException(message, cause)
with NoStackTrace
object SocketException {
private[io] def unapply(cause: js.JavaScriptException): Option[SocketException] = cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("ECONNRESET") =>
Some(new JavaScriptSocketException(cause))
case _ => BindException.unapply(cause).orElse(ConnectException.unapply(cause))
}
private[io] def unapply(cause: js.JavaScriptException): Option[SocketException] =
cause.exception match {
case error: js.Error if error.message.contains("ECONNRESET") =>
Some(new JavaScriptSocketException("Connection reset by peer", cause))
case _ => BindException.unapply(cause).orElse(ConnectException.unapply(cause))
}
}

class BindException(message: String = null, cause: Throwable = null)
extends SocketException(message, cause)
private class JavaScriptBindException(cause: js.JavaScriptException)
extends BindException(cause = cause)
extends BindException("Address already in use", cause)
with NoStackTrace
object BindException {
private[net] def unapply(cause: js.JavaScriptException): Option[BindException] = cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("EADDRINUSE") =>
Some(new JavaScriptBindException(cause))
case _ => None
}
private[net] def unapply(cause: js.JavaScriptException): Option[BindException] =
cause.exception match {
case error: js.Error if error.message.contains("EADDRINUSE") =>
Some(new JavaScriptBindException(cause))
case _ => None
}
}

class ConnectException(message: String = null, cause: Throwable = null)
extends SocketException(message, cause)
private class JavaScriptConnectException(cause: js.JavaScriptException)
extends ConnectException(cause = cause)
extends ConnectException("Connection refused", cause)
with NoStackTrace
object ConnectException {
private[net] def unapply(cause: js.JavaScriptException): Option[ConnectException] = cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("ECONNREFUSED") =>
Some(new JavaScriptConnectException(cause))
case _ => None
}
private[net] def unapply(cause: js.JavaScriptException): Option[ConnectException] =
cause.exception match {
case error: js.Error if error.message.contains("ECONNREFUSED") =>
Some(new JavaScriptConnectException(cause))
case _ => None
}
}

class SocketTimeoutException(message: String = null, cause: Throwable = null)
extends IOException(message, cause)
extends InterruptedIOException(message, cause)
private class JavaScriptSocketTimeoutException(cause: js.JavaScriptException)
extends SocketTimeoutException(cause = cause)
extends SocketTimeoutException("Connection timed out", cause)
with NoStackTrace
object SocketTimeoutException {
private[io] def unapply(cause: js.JavaScriptException): Option[SocketTimeoutException] =
cause match {
case js.JavaScriptException(error: js.Error) if error.message.contains("ETIMEDOUT") =>
cause.exception match {
case error: js.Error if error.message.contains("ETIMEDOUT") =>
Some(new JavaScriptSocketTimeoutException(cause))
case _ => None
}
}

class UnknownHostException(message: String = null, cause: Throwable = null)
extends IOException(message, cause)
private class JavaScriptUnknownException(cause: js.JavaScriptException)
extends UnknownHostException(cause = cause)
private class JavaScriptUnknownHostException(host: String, cause: js.JavaScriptException)
extends UnknownHostException(s"$host: ${UnknownHostException.message}", cause)
with NoStackTrace
object UnknownHostException {
private[io] def unapply(cause: js.JavaScriptException): Option[UnknownHostException] =
cause match {
case js.JavaScriptException(error: js.Error)
if error.message.contains("ENOTFOUND") || error.message.contains("EAI_AGAIN") =>
Some(new JavaScriptUnknownException(cause))
cause.exception match {
case error: js.Error =>
pattern.findFirstMatchIn(error.message).collect { case Regex.Groups(addr) =>
val host =
Option(addr)
.flatMap { addr =>
SocketAddress.fromString(addr).map(_.host).orElse(Host.fromString(addr))
}
.fold("<unknown>")(_.toString)
new JavaScriptUnknownHostException(host, cause)
}
case _ => None
}
private[this] val pattern = raw"(?:ENOTFOUND|EAI_AGAIN)(?: (\S+))?".r
private[net] val message = osMod.`type`() match {
case "Darwin" => "nodename nor servname provided, or not known"
case _ => "Name or service not known"
}
}
1 change: 1 addition & 0 deletions io/jvm/src/main/scala/fs2/io/ioplatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import fs2.io.internal.PipedStreamBuffer
import java.io.{InputStream, OutputStream}

private[fs2] trait ioplatform {
type InterruptedIOException = java.io.InterruptedIOException
type ClosedChannelException = java.nio.channels.ClosedChannelException

/** Pipe that converts a stream of bytes to a stream that will emit a single `java.io.InputStream`,
Expand Down
1 change: 1 addition & 0 deletions io/jvm/src/main/scala/fs2/io/net/net.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package fs2.io

/** Provides support for doing network I/O -- TCP, UDP, and TLS. */
package object net {
type ProtocolException = java.net.ProtocolException
type SocketException = java.net.SocketException
type BindException = java.net.BindException
type ConnectException = java.net.ConnectException
Expand Down
Loading

0 comments on commit b2a90a1

Please sign in to comment.