# Changelog of Bulk URL Status and Redirect Checker (`fecklesspleb/url-status-checker`) Actor

- **URL**: https://apify.com/fecklesspleb/url-status-checker/changelog.md
- **Full Actor documentation**: https://apify.com/fecklesspleb/url-status-checker.md

## Changelog

### Unreleased: invalid input no longer crashes (2026-11-04, local, not built or pushed; based on cloud build 1.0.9)

- **Fix:** input that is not a JSON object (a string, number, `null` or list) crashed the run with exit 91. It now ends with exit 0 and one free status row: `Invalid input, nothing was fetched and no results were charged: the input must be a JSON object, got <type>.`
- **Fix:** wrong-type or unusable values that crashed with exit 91 before anything was fetched now end the same way (exit 0, one free status row with `charged: false`, no `RUN_SUMMARY`, no charged event) with the reason `<field> has the wrong type (<error>).` Covered: `urls` (a list with numbers or other non-text entries, or a number), `maxRedirects`, `maxResults`, `maxConcurrency`.
- The check repeats the exact conversions the run makes, before any request; valid input behaves exactly as before. Network, HTTP and upstream errors are unchanged.
- The deliberate free rows (demo row, rows for URLs that could not be checked, robots.txt rows) are unchanged.
- Memory: `defaultMemoryMbytes` is now 256 (was 1024); min 128 and max 2048 unchanged. README: the start fee is now described at the default 256 MB (was 1 GB) in the At a glance line, the pricing bullet and the examples heading; amounts are unchanged (1 start event).
- `requirements.txt`: `apify` pinned to `>= 4.1.0, < 4.2.0` (was `>= 4.0.0, < 5.0.0`).
- New unit tests: `tests/test_invalid_input.py`.

### Unreleased (1102x), changelog only

- Correction to the 2026-11-02 entry below (build 1.0.6): the README sentence on blocked URLs is no longer in the
  Output section. Build 1.0.7 moved it to a new "Limits" section. No code or README change.

### 2026-11-02 (actor.json version unchanged: 1.0)

#### Added: private-address (SSRF) guard

- On the Apify platform, every request (the URL itself, each redirect hop, and robots.txt) is refused when the host
  resolves to a private, loopback, link-local, carrier-grade NAT, reserved, multicast or unspecified address
  (including IPv4-mapped IPv6), or when any of the host's addresses is one. Same rules as Bulk HTTP Request.
- Such a URL gets a free error row: `error: "private, loopback and link-local addresses are not allowed"`,
  `category: "error"`, `isBroken: true`, `status: "error"` and a `message` starting "Not charged". A redirect hop
  to a private address stops the chain there; the hops before it stay in `redirectChain`.
- A host the guard cannot resolve fails as a DNS error, as before (`error` now reads
  "DNS or connection error: DNS lookup failed for <host>"); the row stays free.
- A robots.txt that redirects to a private address counts as unavailable (no robots.txt rules), like a robots.txt
  that cannot be fetched.
- Local runs (APIFY_IS_AT_HOME not set) are unchanged.

#### Files changed

- `my_actor/common.py`: `ip_blocked`, `host_problem`, `check_public`, `guard_request` (httpx request hook),
  `guard_site`, `Blocked`, `DnsLookupFailed`; `Http._robots_for` checks the site before fetching robots.txt.
- `my_actor/main.py`: the httpx client gets the request hook; `Blocked` becomes a free error row.
- `tests/test_ssrf_guard_1102t.py`: new. README: one sentence on blocked URLs in the Output section.

### 2026-10-30, 10:17 PM EDT (actor.json version unchanged: 1.0), docs only

- README.md (publisher, from Echo's README.proposed.md checked against the code): error rows (connection/DNS
  errors, timeouts, redirect loops, too many redirects), robots.txt-skipped rows and the `{}` / example.com demo row
  are free and carry `status` and `message`; charged rows have neither field. Free rows waive only the per-URL fee:
  the `apify-actor-start` event ($0.00005 per GB of memory) is still charged on every run, so `{}` costs $0.00005 at
  the default 1 GB. Cost examples updated; `RUN_SUMMARY` `rowsCharged` / `rowsFree` documented. This also corrects
  the entry below, which says only that `{}` "succeeds with one free row".

### 2026-10-30 (actor.json version unchanged: 1.0)

#### Changed: rows that are not a real check result are free

These dataset rows are now pushed without the `url` event (not charged):

- **Error rows**: every row with `category: "error"`: DNS or connection errors, timeouts, other request exceptions,
  `redirect loop` and `more than N redirects`.
- **robots.txt-blocked rows** (`error: "not checked: robots.txt disallows it"`).
- **The demo check of the input example**: when the URL list is empty, or is only `https://example.com` (the input
  schema default and prefill, so also what the platform sends for `{}`; `http://`, `www.` and a trailing `/` count
  too). `{}` now succeeds with one free row.

Free rows get two extra fields: `status` (`error`, `skipped` or `warning`) and `message` (starts with "Not charged").
Charged rows (2xx/3xx/4xx/5xx results) have exactly the same fields as before. Free rows still count towards
`maxResults`, and never trigger the max-cost stop. `RUN_SUMMARY` gains `rowsCharged` and `rowsFree`;
`byCategory` now counts only rows that were saved. The status message says how many rows were free.

#### Files changed

- `my_actor/common.py`: `Emitter.push(item, charge=True)`; `charge=False` pushes without an event; `charged` /
  `free` counters.
- `my_actor/main.py`: `ROBOTS_ERROR`, `is_sample()`, `free_status()`; free rows get `status`/`message`; run summary
  and status message.
- `tests/test_free_rows.py` (new): unit tests and end-to-end runs against a local HTTP server.
- `pytest.ini` (new): test configuration.
- `CHANGELOG.md` (new): this file.

README.md is NOT changed by this entry. `.actor/dataset_schema.json` view unchanged (`status`/`message` are not
shown in the table view).
