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

Error handling improvements #570

Merged

Conversation

lewisjkl
Copy link
Contributor

@lewisjkl lewisjkl commented Nov 3, 2022

Updates error handling to close #413.

See attached docs in markdown file in PR for more information.

@daddykotex
Copy link
Contributor

note: I explicitly removed all that codegen noise from my other PR because I know @Baccata already has those changes in #567

@lewisjkl
Copy link
Contributor Author

lewisjkl commented Nov 3, 2022

note: I explicitly removed all that codegen noise from my other PR because I know @Baccata already has those changes in #567

Which codegen noise are you referring to?

@daddykotex
Copy link
Contributor

Screen Shot 2022-11-03 at 16 20 44

You touched like 4 files, and like 100 lines or so but your PR is almost 2k lines

@lewisjkl
Copy link
Contributor Author

lewisjkl commented Nov 3, 2022

Ahh I see, I didn't even look at the github diff (my bad) but yeah I will address that. Thanks!

@lewisjkl
Copy link
Contributor Author

lewisjkl commented Nov 4, 2022

Marking as draft until we get 0.17 CI passing in general, then will update this branch and re-mark as ready for review

@lewisjkl lewisjkl marked this pull request as draft November 4, 2022 15:44
@lewisjkl lewisjkl marked this pull request as ready for review November 8, 2022 20:59
Copy link
Contributor

@Baccata Baccata left a comment

Choose a reason for hiding this comment

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

Great work, loving the docs and the tests 👍

// If Error trait is present (must be 'client' or 'server') and
// the status code is in the client/server error range, then use that alt
val isClientError =
e.value.toLowerCase == "client" && inputStatus >= 400 && inputStatus < 500
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick : if e == Error.Client

}
.collect {
case (Some(key), values) if values.size == 1 => key -> values.head
private val byStatusCode: Int => Option[SchemaAlt[E, _]] = { inputStatus =>
Copy link
Contributor

@Baccata Baccata Nov 9, 2022

Choose a reason for hiding this comment

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

semi nitpick : hotpath

Copy link
Contributor

Choose a reason for hiding this comment

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

I thought about this and even checkout the branch to give it a shot, it got hairy pretty quickly
Will give it another shot, I'm probably missing something

Copy link
Contributor

Choose a reason for hiding this comment

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

second attempt:

diff --git a/modules/core/src/smithy4s/http/ErrorAltPicker.scala b/modules/core/src/smithy4s/http/ErrorAltPicker.scala
index 91c297df..cb57cdad 100644
--- a/modules/core/src/smithy4s/http/ErrorAltPicker.scala
+++ b/modules/core/src/smithy4s/http/ErrorAltPicker.scala
@@ -41,29 +41,51 @@ final class ErrorAltPicker[E](alts: Vector[SchemaAlt[E, _]]) {
     .map(alt => alt.instance.shapeId.name -> alt)
     .toMap[String, SchemaAlt[E, _]]
 
-  private val byStatusCode: Int => Option[SchemaAlt[E, _]] = { inputStatus =>
-    val errorForStatus: Option[SchemaAlt[E, _]] = {
-      val results =
-        alts.filter(_.hints.get(HttpError).exists(_.value == inputStatus))
-      if (results.length == 1) results.headOption else None
-    }
-    lazy val fallbackError: Option[SchemaAlt[E, _]] = {
-      val matchingAlts = alts.filter { alt =>
-        alt.hints.get(Error) match {
-          case Some(e) if alt.hints.get(HttpError).isEmpty =>
-            // If Error trait is present (must be 'client' or 'server') and
-            // the status code is in the client/server error range, then use that alt
-            val isClientError =
-              e.value.toLowerCase == "client" && inputStatus >= 400 && inputStatus < 500
-            val isServerError =
-              e.value.toLowerCase == "server" && inputStatus >= 500 && inputStatus < 600
-            isClientError || isServerError
-          case _ => false
-        }
+  // build a map: status code to alternative
+  // exclude all status code that are used on multiple alternative
+  // in essence, it gives a `Map[Int, SchemaAlt[E, _]]` that's used
+  // for the lookup
+  private val byStatusCode: Int => Option[SchemaAlt[E, _]] = {
+    val perStatusCode: Map[Int, SchemaAlt[E, _]] = alts
+      .flatMap { alt =>
+        alt.hints.get(HttpError).map { he => he.value -> alt }
+      }
+      .groupMap(_._1)(_._2)
+      .collect {
+        // Discard alternative where another alternative has the same http status code
+        case (status, allAlts) if allAlts.size == 1 => status -> allAlts.head
+      }
+    val errorForStatus: Int => Option[SchemaAlt[E, _]] = perStatusCode.get
+
+    lazy val fallbackError: Int => Option[SchemaAlt[E, _]] = {
+      // grab the alt that's annotated with the expected `Error` hint
+      // only if there is only one
+      def forErrorType(expected: Error): Option[SchemaAlt[E, _]] = {
+        val matchingAlts = alts
+          .flatMap { alt =>
+            alt.hints
+              .get(HttpError)
+              .fold(
+                alt.hints.get(Error).collect {
+                  case e if e == expected => alt
+                }
+              )(_ => None)
+
+          }
+        if (matchingAlts.size == 1) matchingAlts.headOption else None
+      }
+      val clientAlt: Option[SchemaAlt[E, _]] = forErrorType(Error.CLIENT)
+      val serverAlt: Option[SchemaAlt[E, _]] = forErrorType(Error.SERVER)
+
+      { intStatus =>
+        if (intStatus >= 400 && intStatus < 500) clientAlt
+        else if (intStatus >= 500 && intStatus < 600) serverAlt
+        else None
       }
-      if (matchingAlts.size == 1) matchingAlts.headOption else None
     }
-    errorForStatus.orElse(fallbackError)
+
+    inputStatus =>
+      errorForStatus(inputStatus).orElse(fallbackError(inputStatus))
   }
 
   def getPreciseAlternative(

side note: I'm not sure we should consider error paths as hot path

Copy link
Contributor

Choose a reason for hiding this comment

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

I can push it if you like it @lewisjkl

@lewisjkl lewisjkl merged commit a06ef72 into disneystreaming:series/0.17 Nov 9, 2022
@lewisjkl lewisjkl deleted the error-handling-improvements branch November 9, 2022 17:11
@Baccata Baccata added this to the 0.17.0 milestone Nov 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants