# Changelog of OpenCorporates Alternative — US Business Registry Scraper (`devilscrapes/opencorporates-alternative-scraper`) Actor

- **URL**: https://apify.com/devilscrapes/opencorporates-alternative-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/opencorporates-alternative-scraper.md

## Changelog

All notable changes to this Actor will be documented in this file.

### \[0.3] - 2026-09-03

#### Fixed

- **A malformed 2xx response body no longer crashes the whole run.**
  `fleet_status.py` flagged 8 of the last 52 customer runs (30d) FAILED
  against live build 0.2.2, with our own runs clean (4/4 SUCCEEDED) — the
  failures are customer traffic and outside our own run history (see
  `reference-fleet-health-signal`). `client._get_with_retry` called
  `response.json()` *outside* the `try`/`except RequestException` block
  that guards `session.get()`. A state SODA host answering HTTP 200 with a
  non-JSON body — a CDN/WAF rate-limit interstitial, a truncated reply
  under load — raises `curl_cffi.requests.exceptions.JSONDecodeError` (a
  `RequestException` subclass) from that unguarded call. Because it
  wasn't caught there, it propagated straight past
  `_fetch_query_jurisdiction`'s `except RuntimeError` guard and crashed
  the entire (query, jurisdiction) batch — the same fault-isolation defect
  shape fixed across the fleet on 09-01/09-02 (kick-chat-archive,
  threads-reply-tree, leboncoin, workday, zoopla, twitch,
  standvirtual), just triggered by a decode failure instead of a network
  exception. Reproduced locally: a mocked 200 response whose `.json()`
  raises `JSONDecodeError` escaped `_get_with_retry` uncaught before this
  fix. Now `response.json()` is decoded inside the same `try` block, so a
  decode failure is retried exactly like any other network hiccup and
  only surfaces as a graceful per-jurisdiction `RuntimeError` after
  `MAX_RETRIES` is exhausted — it never crashes the run.

#### Added

- `tests/test_client.py::test_get_with_retry_malformed_json_then_success`
  and `::test_get_with_retry_malformed_json_exhausts_retries_gracefully`
  pin the fix at the `_get_with_retry` layer.

### \[0.2] - 2026-08-26

#### Fixed

- **A genuine no-match search no longer FAILS the run.** `fleet_status.py`
  flagged 11 of the last 57 customer runs (30d) FAILED, with our own runs
  clean (1/1 SUCCEEDED) — the failures are customer traffic against build
  0.1.2. `main()` raised `SystemExit(1)` any time the summed row count across
  every `(query, jurisdiction)` pair was zero (spec REQ-8, "fail loud on
  zero rows"), charging the customer `actor-start` and then reporting
  FAILED. Reproduced locally: a made-up company name against the live CO
  SODA endpoint completes the HTTP request successfully (no retries, no
  exception) and still exited 1. This Actor covers only 4 states
  (NY/CO/CT/OR) with `jurisdictions` defaulting to all four, so an ordinary
  batch-verify query for a company not registered in any of them is a
  *legitimate* zero-match answer, not a failure — the exact pattern that
  already delisted `reverb-sold-listings` once (see
  `ops/os/EMPTY-IS-NOT-A-FAILURE-2026-08-19.md`).
  `_fetch_query_jurisdiction` now returns `(rows, completed)` and `_run`
  returns a `RunOutcome(total, any_completed)`. `main()` only fails loud
  when `any_completed` is `False` — i.e. every single search errored out
  and we never got a real answer from any state host. A completed search
  that matched nothing now finishes SUCCEEDED with a status message naming
  what was searched.

#### Added

- `tests/test_empty_result_succeeds.py` — pins the no-match-succeeds
  behaviour at both the `_run` and `main()` layers, and pins that `main()`
  still fails loud when every search genuinely errors (all state hosts
  unreachable), and that the happy path (`Done — N entity row(s)`) is
  unchanged.

### \[0.1] - 2026-07-20

#### Fixed

- **Zero-match "Try for free" trap.** `input_schema.json`'s `searchQueries`
  prefill was `"Acme Robotics LLC"` — a name that doesn't exist in any of
  the 4 state registries (NY/CO/CT/OR), so every customer who ran the
  Actor with the default sample input got a guaranteed REQ-8 zero-match
  `FAILED` run. This is the most likely explanation for the 2/2 recent
  public-run failures flagged by the daily health report (30-day
  `publicActorRunStats`). Replaced with `"Blue Sky Consulting LLC"`,
  verified live against all 4 SODA endpoints to return real matches.
- **`client._get_with_retry` never retried network-level exceptions.**
  design.md's own REQ-5 text says "retry on HTTP 429, 5xx, and
  `curl_cffi` network exceptions", but the implementation only retried
  on HTTP status codes — a bare connection timeout/reset talking to any
  one of the 4 independent state open-data hosts raised an uncaught
  `curl_cffi.requests.exceptions.RequestException` past
  `_fetch_query_jurisdiction`'s `except RuntimeError` guard and crashed
  the entire run (losing results from every other jurisdiction/query in
  the same batch). Now caught and retried with the same exponential
  backoff as retriable HTTP statuses; only raises `RuntimeError` (which
  the caller already treats as a graceful per-jurisdiction skip) after
  `MAX_RETRIES` is exhausted.

### \[Unreleased]

#### Added

- Scaffold: `.actor/` (actor.json, input\_schema.json, output\_schema.json,
  dataset\_schema.json, pay\_per\_event.json, Dockerfile), `src/models.py`
  (fully implemented `ActorInput` + `Officer` + `ResultRow`), stubs for
  `adapters.py` / `client.py` / `parser.py` / `main.py` (signatures per
  design.md, filled in during T04-T08), `src/scraper.py` (ADR-0004
  verifier stub), `pyproject.toml`, `README.md`, `tests/fixtures/input.qa.json`.
- Two PPE events: `actor-start` ($0.005), `result-row` ($0.005).
