Skip to content

Commit 824949f

Browse files
Merge pull request #5682 from openstates/de-bills-retry-json-decode-handling
DE: bills: add retry handling around requests
2 parents a7fe600 + bd5b56e commit 824949f

1 file changed

Lines changed: 63 additions & 13 deletions

File tree

scrapers/de/bills.py

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import datetime as dt
2+
from functools import partial
23
import json
4+
from json.decoder import JSONDecodeError
5+
from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
6+
import random
7+
import time
38

49
import requests
510
from openstates.scrape import Scraper, Bill, VoteEvent
@@ -33,6 +38,40 @@ class DEBillScraper(Scraper, LXMLMixin):
3338
"senatedems": "https://senatedems.delaware.gov/members/senate-district-",
3439
}
3540

41+
def decode_and_retry_request(
42+
self, log_label, request_method, attempt=1, retries=3, raise_exception=True
43+
):
44+
"""
45+
Delaware is giving us a lot of malformed responses that are HTTP OK nonetheless
46+
Normally flakey requests could be handled by http-resilience mode, but because the HTTP response
47+
is normal (just empty/not-decodable-to-JSON)
48+
So we have several places where we need to retry the decode step
49+
"""
50+
try:
51+
response = request_method()
52+
data = json.loads(response.content.decode("utf-8"))
53+
return data
54+
except (JSONDecodeError, RequestsJSONDecodeError) as error:
55+
if attempt < retries:
56+
min_seconds = 5
57+
max_seconds = 10
58+
delay = random.uniform(min_seconds, max_seconds)
59+
self.warning(
60+
f"Failed to fetch {log_label} on attempt {attempt}, retrying w delay {delay}"
61+
)
62+
time.sleep(delay)
63+
return self.decode_and_retry_request(
64+
log_label, request_method, attempt + 1, retries, raise_exception
65+
)
66+
else:
67+
self.warning(
68+
f"Failed to fetch {log_label} on attempt {attempt} of {retries}, giving up"
69+
)
70+
if raise_exception:
71+
raise error
72+
else:
73+
return None
74+
3675
def scrape(self, session=None):
3776
self.headers = {
3877
"x-oxylabs-force-headers": "1",
@@ -67,7 +106,13 @@ def scrape(self, session=None):
67106
bills_and_votes = []
68107
page_number = 1
69108
while True:
70-
page = self.post_search(session, page_number, per_page)
109+
post_search = partial(
110+
self.post_search,
111+
session=session,
112+
page_number=page_number,
113+
per_page=per_page,
114+
)
115+
page = self.decode_and_retry_request("post_search", post_search)
71116
if not page["Data"]:
72117
self.info("Found no more bills in pagination")
73118
break
@@ -296,18 +341,20 @@ def scrape_votes(self, bill, legislation_id, session):
296341
)
297342
form = {"legislationId": legislation_id, "sort": "", "group": "", "filter": ""}
298343
self.info(f"Searching for votes for {bill.identifier}")
299-
response = self.session.post(
344+
request_method = partial(
345+
self.session.post,
300346
url=votes_url,
301347
data=form,
302348
allow_redirects=True,
303349
verify=False,
304350
headers=self.headers,
305351
)
306-
if response.content:
307-
page = json.loads(response.content.decode("utf-8"))
308-
if page["Total"] > 0:
309-
for row in page["Data"]:
310-
yield from self.scrape_vote(bill, row["RollCallId"], session)
352+
page = self.decode_and_retry_request(
353+
"scrape_votes", request_method, retries=1, raise_exception=False
354+
)
355+
if page and page["Total"] > 0:
356+
for row in page["Data"]:
357+
yield from self.scrape_vote(bill, row["RollCallId"], session)
311358

312359
def scrape_vote(self, bill, vote_id, session):
313360
vote_url = (
@@ -316,14 +363,17 @@ def scrape_vote(self, bill, vote_id, session):
316363
form = {"rollCallId": vote_id, "sort": "", "group": "", "filter": ""}
317364

318365
self.info(f"Fetching vote {vote_id} for {bill.identifier}")
319-
response = self.session.post(
366+
request_method = partial(
367+
self.session.post,
320368
url=vote_url,
321369
data=form,
322370
allow_redirects=True,
323371
verify=False,
324372
headers=self.headers,
325373
)
326-
page = response.json()
374+
page = self.decode_and_retry_request(
375+
"scrape_vote", request_method, retries=1, raise_exception=False
376+
)
327377

328378
if page:
329379
roll = page["Model"]
@@ -419,14 +469,15 @@ def scrape_actions(self, bill, legislation_id):
419469
)
420470
form = {"legislationId": legislation_id, "sort": "", "group": "", "filter": ""}
421471
self.info(f"Fetching actions for {bill.identifier}")
422-
response = self.session.post(
472+
request_method = partial(
473+
self.session.post,
423474
url=actions_url,
424475
data=form,
425476
allow_redirects=True,
426477
verify=False,
427478
headers=self.headers,
428479
)
429-
page = response.json()
480+
page = self.decode_and_retry_request("scrape_actions", request_method)
430481
for row in page["Data"]:
431482
action_name = row["ActionDescription"]
432483
action_date = dt.datetime.strptime(
@@ -558,8 +609,7 @@ def post_search(self, session, page_number, per_page):
558609
verify=False,
559610
headers=self.headers,
560611
)
561-
page = response.json()
562-
return page
612+
return response
563613

564614
def mime_from_link(self, link):
565615
if "HtmlDocument" in link:

0 commit comments

Comments
 (0)