Skip to content

Next release: Vale of White Horse, Sunderland, Bolsover, Solihull, Exeter fixes + config redaction - #2241

Merged
robbrad merged 15 commits into
masterfrom
claude/open-issues-rkmshu
Sep 24, 2026
Merged

robbrad merged 15 commits into
masterfrom
claude/open-issues-rkmshu

Conversation

@robbrad

@robbrad robbrad commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

Consolidates the vetted open PRs into the next release, tested together on one branch.

  • chore: bump docker/login-action 4.5.2 → 4.6.0, github/codeql-action 4.37.4 → 4.38.1 (from chore: bump docker/login-action from 4.5.2 to 4.6.0 #2227, chore: bump github/codeql-action from 4.37.4 to 4.38.1 #2237, dependabot)
  • fix(vale-of-white-horse): don't infer a collection date into a bank holiday or Christmas week — the page publishes the current fortnight's pair of collections rather than the next occurrence of each bin, so a past date is rolled forward by 14 days; that's wrong when it lands in a week the council collects on a different day (bank holidays, and the 24 Dec–17 Jan slippage period), which only the page itself knows. Also fixes a separate bug where an early-January page still showing December's date was read as eleven months in the future instead of last year's. (from fix: ValeofWhiteHorseCouncil: don't infer a collection date into a bank holiday week #2229, credit @dpage, follows up on ValeofWhiteHorseCouncil: returns the previous collection date for the bin collected earlier in the fortnight #2213) — live-verified passing.
  • fix(sunderland): use curl_cffi to pass Cloudflare's TLS fingerprint challenge, plus a full realistic header set — since mid-September the council's site 403s any client whose TLS handshake doesn't look like a real browser. Swaps the session for curl_cffi impersonating Chrome and, matching the existing pattern for Gateshead/Powys (same GOSS iCM platform, also Cloudflare-fronted), sends a complete browser-like header set rather than just a User-Agent. Not conclusively verifiable from this branch's own CI: the live integration run still gets Cloudflare's challenge page (cf-mitigated: challenge), which matches a known, already-documented limitation for these councils — Cloudflare's IP reputation scoring flags GitHub-hosted runner IPs regardless of client sophistication (see the comment already in PowysCouncil.py/GatesheadCouncil.py). Real-world verification will need to come from an actual deployment. (from fix(councils): use curl_cffi for SunderlandCityCouncil to pass Cloudflare TLS fingerprint challenge #2233, credit @jonny190, targets SunderlandCityCouncil: 403 from Cloudflare TLS fingerprint challenge since 15 Sep 2026 (curl_cffi fix tested) #2232)
  • fix(bolsover): use a continuous fortnight index for bin parity, not a relative week number — the old parity calc was positioned relative to "today", so the same calendar date could get a different (wrong) bin type depending on when the scraper ran, and didn't reliably survive ISO week-53 year boundaries. Anchored to a fixed epoch validated against the council's published calendar instead. (from fix(BolsoverCouncil): bin type flips + days jump by +6 due to relative parity #2236, credit @JoeGandy) — live-verified passing.
  • fix(ha): redact household and endpoint values from config-flow diagnostics — HA config-flow/setup logs previously included full config dicts and CLI args (postcodes, UPRNs, house numbers, private URLs) at debug/info level, which HA diagnostics can collect and share outside the local install. (from fix: redact sensitive configuration values from logs #2239, credit @dajiaohuang) — CI-verified passing on Python 3.14 across all supported HA versions (2026.2 through 2026.9, plus dev).
  • fix(solihull): handle bin cards with no collection history — a bin card with no collection history yet (e.g. a newly added food caddy) renders only the "Next collection" row instead of both "Last collected" and "Next collection", so the previous fixed-index lookup went out of range. Reads the last of whichever date rows are present instead, and skips a card with none rather than crashing. (fixes Solihull Council: Unexpected error: list index out of range #2235) — live-verified passing.
  • fix(exeter): send realistic browser headers on the address-finder request — the request previously sent no headers at all (not even a User-Agent), which points at bot/fingerprint detection given the Exeter City Council 403 error #2240 reporter could load the site fine in an ordinary browser. Adds the same kind of realistic header set already used for Gateshead/Powys. Also not conclusively verifiable from this branch's own CI — same known limitation as Sunderland: the site's Azure Application Gateway still 403s the runner's IP with a generic static error page, not a payload-validation error, which is a source-IP issue rather than proof the fix is wrong. (targets Exeter City Council 403 error #2240)

Investigated but not included

  • Canterbury (Canterbury Council - 403 Error #2215, 403 error): tried switching the retired "Beta" API stage to "prod" (the stage another open-source project confirmed the council's site now uses), but this branch's own live CI still got a clean ForbiddenException from the prod stage too. Comparing against that other project's working implementation shows the real gap: it derives a genuine uprn+usrn pair from a postcode+house-number address lookup, while this scraper only ever accepts a raw UPRN with a hardcoded placeholder usrn. Matching that flow means changing this scraper's expected inputs (postcode+house number instead of UPRN alone) — a breaking change for existing users, not a same-shape fix — so it's reverted out of this release and left as a larger follow-up, with findings posted on Canterbury Council - 403 Error #2215.
  • Buckinghamshire (Buckinghamshire Council - Empty Response error from council #2238 empty table, Unexpected error in async_setup_entry: Timeout while updating data #2228 timeout): this branch's own live CI ran the existing scraper unmodified against the real site and it passed cleanly - the collection table was populated, no timeout. Looks like both reports were transient/council-side rather than a code defect; commented on both issues asking reporters to confirm if still occurring.

Not included

#2180 ("harden South Kesteven scraper and Home Assistant runtime") remains excluded. It's still the same 199-file, +13.5k/-3.6k change bundling the actual SKDC fix with an unrelated cross-council print-redaction sweep, a new test harness, and a rewrite of the release/CI pipeline. I asked the author in July whether they'd split it into reviewable pieces; there's been no response in two months, so it's left as-is for a maintainer to decide on separately.

Test plan

  • New regression tests all pass: 7 for Vale of White Horse (bank holiday/Christmas/January-rollover cases), 9 for Bolsover (parity alternation, ISO week-53/52 boundaries, green-bin suspension), 4 for Solihull (with/without collection history, mixed cards, no-date-rows skip)
  • Full non-live uk_bin_collection unit suite green (no regressions)
  • black --check clean on all changed files
  • Live CI integration run: Bolsover, Buckinghamshire, Solihull, Vale of White Horse all passed live
  • custom_components (HA redact fix) tests: green on Python 3.14 across every supported HA version in the compatibility matrix
  • Sunderland and Exeter fail live from CI specifically (their respective edge/gateway layers challenge or block the runner's IP itself, not the client); neither can be conclusively verified from this environment — see notes above

🤖 Generated with Claude Code

https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4

Summary by CodeRabbit

  • Bug Fixes
    • Improved bin schedules for Bolsover, Solihull, and Vale of White Horse, including alternating collection rounds, handling missing collection history, and accounting for rescheduled weeks and year boundaries.
    • Improved Sunderland and Exeter council lookups for more reliable address requests.
  • Security & Privacy
    • Sensitive configuration details are now redacted from diagnostic logs.
  • Chores
    • Updated security-analysis and release workflow actions.

dependabot Bot and others added 6 commits September 24, 2026 21:47
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.5.2 to 4.6.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@v4.5.2...v4.6.0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.4 to 4.38.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@v4.37.4...v4.38.1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…oliday or Christmas week

The page publishes the current fortnight's pair of collections rather
than the next occurrence of each bin, so a past date is rolled forward
by 14 days. That's wrong when the roll-forward lands in a week the
council collects on a different day - which only the page knows. Skip
publishing a guess for a bank-holiday week (checked with the existing
is_holiday()) or the 24 Dec-17 Jan slippage period, where the council's
leaflet shows collections running one to two days late well past the
last holiday.

Also fixes a separate bug: the page carries no year, so in early
January it still shows the current fortnight's December date, which
parsed as this year's December was returned as eleven months away. A
page date more than six months ahead is now read as last year's.

Credit: dpage (#2229). Follows up on #2213.
…hallenge

Since mid-September 2026 the council's site serves a Cloudflare managed
challenge (cf-mitigated: challenge) on every page to clients whose TLS
fingerprint doesn't look like a browser - plain requests/curl get 403
regardless of headers, source IP, or User-Agent. The form itself is
still a plain HTML postback wizard needing no JavaScript execution, so
driving it through a curl_cffi session that impersonates Chrome's TLS
handshake is sufficient.

Adds curl_cffi as a dependency (ships manylinux/musllinux wheels, so it
installs without a compiler on the HA Alpine image too).

Credit: jonny190 (#2233). Fixes #2232.
… relative week number

The scraper computed which bin type (Black, or Burgundy+Green) is
collected on a date using a week index (1-4) positioned relative to
"today". The same calendar date could get different parities on
consecutive runs, and the phase didn't reliably survive ISO week-53
year boundaries.

Anchor to a fixed epoch (2024-01-08, the Monday found by testing
against the council's published calendar to match their "Week 1"
phase) and derive parity from a continuous fortnight count from there,
so consecutive weeks always alternate regardless of when the scraper
runs or where a year boundary falls.

Verified against Bolsover's published calendar B (UPRN 100030066827):
confirmed Black Bin on 22/09/2026, Burgundy+Green on 29/09/2026.

Credit: JoeGandy (#2236).
…stics

Home Assistant config-flow and setup logs previously included complete
config dictionaries and CLI argument values at debug/info level -
postcodes, UPRNs, house numbers, private council/Selenium URLs, and
user-supplied names. Those records are often collected for HA
diagnostics and shared outside the local installation.

Adds redact_config_data(), applied everywhere a config dict or
Selenium URL was logged, and drops raw UKBCD argument values from logs
in favour of an argument count. Config-flow behavior is unchanged;
only what reaches the logs is affected.

Credit: dajiaohuang (#2239).
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: dcba4cc7-a4c5-4cbb-9a7b-f80de4fa9025

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb64ea and 0a9ab8b.

⛔ Files ignored due to path filters (1)
  • poetry.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • .github/workflows/codeql-analysis.yml
  • .github/workflows/release.yml
  • custom_components/uk_bin_collection/__init__.py
  • custom_components/uk_bin_collection/config_flow.py
  • custom_components/uk_bin_collection/const.py
  • custom_components/uk_bin_collection/tests/test_config_flow.py
  • pyproject.toml
  • uk_bin_collection/tests/test_bolsover_council.py
  • uk_bin_collection/tests/test_solihull_council.py
  • uk_bin_collection/tests/test_vale_of_white_horse_council.py
  • uk_bin_collection/uk_bin_collection/councils/BolsoverCouncil.py
  • uk_bin_collection/uk_bin_collection/councils/ExeterCityCouncil.py
  • uk_bin_collection/uk_bin_collection/councils/SolihullCouncil.py
  • uk_bin_collection/uk_bin_collection/councils/SunderlandCityCouncil.py
  • uk_bin_collection/uk_bin_collection/councils/ValeofWhiteHorseCouncil.py
 __________________________________________________
< Making NVIDIA proud with all this bug detection. >
 --------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
5 2 3 0
View the full list of 2 ❄️ flaky test(s)
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[ExeterCityCouncil]

Flake rate in main: 100.00% (Passed 0 times, Failed 623 times)

Stack Traces | 0.497s run time
fixturefunc = <function scrape_step at 0x7fae8cdd4670>
request = <FixtureRequest for <Function test_scenario_outline[ExeterCityCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fae8cc263c0>, 'headless_mode': 'True', 'local_browser': 'False', 'selenium_url': 'http://localhost:4444'}

    def call_fixture_func(
        fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
    ) -> FixtureValue:
        if inspect.isgeneratorfunction(fixturefunc):
            fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc)
            generator = fixturefunc(**kwargs)
            try:
                fixture_result = next(generator)
            except StopIteration:
                raise ValueError(f"{request.fixturename} did not yield a value") from None
            finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
            request.addfinalizer(finalizer)
        else:
            fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
>           fixture_result = fixturefunc(**kwargs)
                             ^^^^^^^^^^^^^^^^^^^^^

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.14/lib/python3.14.../site-packages/_pytest/fixtures.py:1005: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../tests/step_defs/test_validate_council.py:101: in scrape_step
    context.parse_result = CollectData.run()
                           ^^^^^^^^^^^^^^^^^
uk_bin_collection/uk_bin_collection/collect_data.py:109: in run
    return self.client_code(
uk_bin_collection/uk_bin_collection/collect_data.py:130: in client_code
    return get_bin_data_class.template_method(address_url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
uk_bin_collection/uk_bin_collection/get_bin_data.py:64: in template_method
    bin_data_dict = self.get_and_parse_data(this_url, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
uk_bin_collection/uk_bin_collection/get_bin_data.py:87: in get_and_parse_data
    bin_data_dict = self.parse_data("", url=address_url, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../uk_bin_collection/councils/ExeterCityCouncil.py:78: in parse_data
    response.raise_for_status()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [403]>

    def raise_for_status(self) -> None:
        """Raises :class:`HTTPError`, if one occurred."""
    
        http_error_msg = ""
        if isinstance(self.reason, bytes):
            # We attempt to decode utf-8 first because some servers
            # choose to localize their reason strings. If the string
            # isn't utf-8, we fall back to iso-8859-1 for all other
            # encodings. (See PR #3538)
            try:
                reason = self.reason.decode("utf-8")
            except UnicodeDecodeError:
                reason = self.reason.decode("iso-8859-1")
        else:
            reason = self.reason
    
        if 400 <= self.status_code < 500:
            http_error_msg = (
                f"{self.status_code} Client Error: {reason} for url: {self.url}"
            )
    
        elif 500 <= self.status_code < 600:
            http_error_msg = (
                f"{self.status_code} Server Error: {reason} for url: {self.url}"
            )
    
        if http_error_msg:
>           raise HTTPError(http_error_msg, response=self)
E           requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://exeter.gov..../repositories/hidden-pages/address-finder/?qsource=UPRN&qtype=bins&term=100040212270

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.14/lib/python3.14.../site-packages/requests/models.py:1167: HTTPError
uk_bin_collection.tests.step_defs.test_validate_council::test_scenario_outline[SunderlandCityCouncil]

Flake rate in main: 59.42% (Passed 254 times, Failed 372 times)

Stack Traces | 0.401s run time
fixturefunc = <function scrape_step at 0x7fbf62642980>
request = <FixtureRequest for <Function test_scenario_outline[SunderlandCityCouncil]>>
kwargs = {'context': <test_validate_council.Context object at 0x7fbf62619be0>, 'headless_mode': 'True', 'local_browser': 'False', 'selenium_url': 'http://localhost:4444'}

    def call_fixture_func(
        fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
    ) -> FixtureValue:
        if inspect.isgeneratorfunction(fixturefunc):
            fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc)
            generator = fixturefunc(**kwargs)
            try:
                fixture_result = next(generator)
            except StopIteration:
                raise ValueError(f"{request.fixturename} did not yield a value") from None
            finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
            request.addfinalizer(finalizer)
        else:
            fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
>           fixture_result = fixturefunc(**kwargs)
                             ^^^^^^^^^^^^^^^^^^^^^

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.14/lib/python3.14.../site-packages/_pytest/fixtures.py:1005: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../tests/step_defs/test_validate_council.py:101: in scrape_step
    context.parse_result = CollectData.run()
                           ^^^^^^^^^^^^^^^^^
uk_bin_collection/uk_bin_collection/collect_data.py:109: in run
    return self.client_code(
uk_bin_collection/uk_bin_collection/collect_data.py:130: in client_code
    return get_bin_data_class.template_method(address_url, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
uk_bin_collection/uk_bin_collection/get_bin_data.py:64: in template_method
    bin_data_dict = self.get_and_parse_data(this_url, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
uk_bin_collection/uk_bin_collection/get_bin_data.py:87: in get_and_parse_data
    bin_data_dict = self.parse_data("", url=address_url, **kwargs)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../uk_bin_collection/councils/SunderlandCityCouncil.py:64: in parse_data
    r.raise_for_status()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Response [403]>

    def raise_for_status(self):
        """Raise an error if status code is not in [200, 400)"""
        if not self.ok:
>           raise HTTPError(f"HTTP Error {self.status_code}: {self.reason}", 0, self)
E           curl_cffi.requests.exceptions.HTTPError: HTTP Error 403:

../../../..../pypoetry/virtualenvs/uk-bin-collection-EwS6Gn8s-py3.14/lib/python3.14.../curl_cffi/requests/models.py:241: HTTPError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

A bin card with no collection history yet (e.g. a newly added food
caddy) renders only the "Next collection" row instead of both "Last
collected" and "Next collection", so the fixed div.mt-1[1] index went
out of range (#2235). Read the last of whichever date rows are
present instead, and skip a card with none rather than crashing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
@robbrad robbrad changed the title Next release: Vale of White Horse, Sunderland, Bolsover fixes + config redaction Next release: Vale of White Horse, Sunderland, Bolsover, Solihull fixes + config redaction Sep 24, 2026
Dumps step timings and, when the collection table comes back empty,
the raw HTML fragment and table candidates found in it. Sandbox
network access can't reach the council's site directly, so this rides
the live integration-test CI job to gather what's actually changed.
Will be removed once the real fix lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
… stage

The council retired the "Beta" API stage this scraper called; it now
403s unconditionally regardless of the uprn/usrn payload sent,
matching what was reported in #2215. Their own front end now calls
the "prod" stage of the same API instead (confirmed via a fix another
open-source waste-collection project independently landed for the
same endpoint), so switch to that, add the Origin/Referer headers
their site sends, and drop the now-redundant extra json.loads of the
"dates" field the prod stage already returns as a proper object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
The live integration test on this branch shows the curl_cffi Chrome
impersonation from #2233 still gets a 403 on the very first GET, not
later in the form flow. Dumping the response status, headers (looking
for cf-mitigated) and a body snippet via the live CI job to see
whether Cloudflare has moved to a JS challenge that a TLS-fingerprint
fix alone can't pass. Will be removed once the real fix lands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
…sonation

Matches the pattern already used for Gateshead and Powys (the same
GOSS iCM platform, also Cloudflare-fronted): Cloudflare scores on
header completeness/IP reputation alongside TLS fingerprint, so a
curl_cffi Chrome impersonation with only a bare User-Agent is weaker
evidence than it needs to be. Also documents the known, unfixable-in-
client limitation that a flagged datacenter IP (e.g. a CI runner) can
still get challenged regardless - this live branch's own CI run hit
exactly that ("cf-mitigated: challenge", a Turnstile interstitial)
even with the TLS impersonation from #2233 already in place.

Also adds temporary diagnostics to Canterbury's still-failing 403 on
the new "prod" endpoint, to see whether it's the same kind of
IP-reputation block or a genuine remaining payload/response mismatch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
The "prod" endpoint swap (from a720e4e) doesn't actually fix #2215 -
this branch's own live CI shows a clean AWS API Gateway
ForbiddenException ({"message":"Forbidden"}, via CloudFront) on the
prod endpoint too, not a bot/Cloudflare-style challenge. Comparing
against another open-source project's working implementation of the
same API shows why: their collection call sends a real uprn+usrn pair
obtained from a postcode+house-number address lookup
(trsewmllv7.execute-api.../dev/address), not a hardcoded usrn="1".
This scraper currently only accepts a UPRN (no postcode/house number),
so matching that flow means changing this scraper's expected inputs -
a breaking change for existing users, not a same-shape fix. Reverting
to the pre-existing behaviour (a clear, honest 403 error) rather than
shipping an endpoint change that doesn't resolve the issue; the real
fix is a larger, separate piece of work, flagged on #2215.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
The live CI run confirmed the scraper works against the real site
unmodified - #2238/#2228 look transient rather than a code defect
(commented on both issues). Removing the diagnostic prints now that
they've served their purpose; file is back to its pre-diagnostic state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
…uest

#2240 reports a 403 from this endpoint, while the reporter could load
the council's site fine in an ordinary browser - the request currently
sends no headers at all (not even a User-Agent), which points at
bot/fingerprint detection rather than an outage. Adds the same kind of
realistic header set already used for other councils behind similar
protection (Gateshead, Powys). Also adds temporary diagnostics on the
UPRN-lookup path (the one the traceback in #2240 hit) to confirm via
live CI whether this actually clears the block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
Live CI still gets a 403 with the realistic headers in place, but it's
a plain Microsoft-Azure-Application-Gateway static error page - not a
bot-challenge or a payload-validation error - which matches the same
flagged-datacenter-IP pattern already documented for Sunderland in
this same release. Keeping the header fix (strictly better than
sending no headers at all) but noting real-world verification has to
come from an actual deployment, not this CI environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MaCS29izthZp2dwtvLwf4
@robbrad robbrad changed the title Next release: Vale of White Horse, Sunderland, Bolsover, Solihull fixes + config redaction Next release: Vale of White Horse, Sunderland, Bolsover, Solihull, Exeter fixes + config redaction Sep 24, 2026
@robbrad robbrad mentioned this pull request Sep 24, 2026
4 tasks done
@robbrad
robbrad merged commit fe88244 into master Sep 24, 2026
23 of 24 checks passed
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.

Solihull Council: Unexpected error: list index out of range

2 participants