Repository: internetarchive/openlibrary

Issue:
Handle Upstream Lending & API exceptions
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

### Describe the problem that you'd like solved
<!-- A clear and concise description of what you want to happen. -->
Relates to #3429 and #8371, when upstream services decide to raise an error and return a non 200 response, this often results in exceptions being raised for Open Library patrons which appear as generic error screens that are not useful or descriptive.

e.g. https://sentry.archive.org/organizations/ia-ux/issues/46145/?project=7&query=lending&referrer=issue-stream&statsPeriod=14d

### Proposal
Proposing that both in petabox and Open Library the code be brought into alignment so that different classes of upstream errors are captured and handled independently so we can have better visibility into actual system errors (like outages) and decrease confusion for patrons.  

### Stakeholders
<!-- @ tag stakeholders of this bug -->
@bfalling @judec 




Discussion:

Resolving patch:
diff --git a/openlibrary/core/lending.py b/openlibrary/core/lending.py
--- a/openlibrary/core/lending.py
+++ b/openlibrary/core/lending.py
@@ -42,6 +42,12 @@
 MAX_IA_RESULTS = 1000
 
 
+class PatronAccessException(Exception):
+    def __init__(self, message="Access to this item is temporarily locked."):
+        self.message = message
+        super().__init__(self.message)
+
+
 config_ia_loan_api_url = None
 config_ia_xauth_api_url = None
 config_ia_availability_api_v2_url = None
@@ -207,6 +213,11 @@ def s3_loan_api(s3_keys, ocaid=None, action='browse', **kwargs):
     data = s3_keys | kwargs
 
     response = requests.post(url + params, data=data)
+    # We want this to be just `409` but first
+    # `www/common/Lending.inc#L111-114` needs to
+    # be updated on petabox
+    if response.status_code in [400, 409]:
+        raise PatronAccessException()
     response.raise_for_status()
     return response
 
diff --git a/openlibrary/plugins/upstream/borrow.py b/openlibrary/plugins/upstream/borrow.py
--- a/openlibrary/plugins/upstream/borrow.py
+++ b/openlibrary/plugins/upstream/borrow.py
@@ -173,29 +173,23 @@ def POST(self, key):
             stats.increment('ol.loans.leaveWaitlist')
             raise web.redirect(edition_redirect)
 
-        # Intercept a 'borrow' action if the user has already
-        # borrowed the book and convert to a 'read' action.
-        # Added so that direct bookreader links being routed through
-        # here can use a single action of 'borrow', regardless of
-        # whether the book has been checked out or not.
-        elif user.has_borrowed(edition):
-            action = 'read'
-
-        elif action in ('borrow', 'browse'):
+        elif action in ('borrow', 'browse') and not user.has_borrowed(edition):
             borrow_access = user_can_borrow_edition(user, edition)
 
             if not (s3_keys and borrow_access):
                 stats.increment('ol.loans.outdatedAvailabilityStatus')
                 raise web.seeother(error_redirect)
 
-            lending.s3_loan_api(
-                s3_keys, ocaid=edition.ocaid, action='%s_book' % borrow_access
-            )
-            stats.increment('ol.loans.bookreader')
-            stats.increment('ol.loans.%s' % borrow_access)
-            action = 'read'
+            try:
+                lending.s3_loan_api(
+                    s3_keys, ocaid=edition.ocaid, action='%s_book' % borrow_access
+                )
+                stats.increment('ol.loans.bookreader')
+                stats.increment('ol.loans.%s' % borrow_access)
+            except lending.PatronAccessException as e:
+                stats.increment('ol.loans.blocked')
 
-        if action == 'read':
+        if action in ('borrow', 'browse', 'read'):
             bookPath = '/stream/' + edition.ocaid
             if i._autoReadAloud is not None:
                 bookPath += '?_autoReadAloud=show'


Test patch: