# Changelog of Article Extractor: News & Blog Article Text Scraper (`fecklesspleb/article-extractor`) Actor

- **URL**: https://apify.com/fecklesspleb/article-extractor/changelog.md
- **Full Actor documentation**: https://apify.com/fecklesspleb/article-extractor.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: `articleUrls` and `sectionUrls` (a number, boolean or a list with numbers), `maxArticlesPerSection`, `minWords`, `maxArticles`, `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 existing free "Not charged: no article was extracted" status row is unchanged.
- Memory unchanged: default 256, min 128, max 4096.
- `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`.

### 2026-11-02 (actor.json version unchanged: 1.0): private-address (SSRF) guard

- On the Apify platform, every request (article URLs, section pages, article links found on them, 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 an article URL is skipped like other unreadable URLs: no row, not charged, counted in `failed`, logged as
  `not fetched: private, loopback and link-local addresses are not allowed`. A section page with such an address is
  skipped with a warning. When no article is extracted, the free status row's reason now reads "could not be read
  (HTTP error, robots.txt, a private address or not an HTML page)".
- A host the guard cannot resolve fails as before ("DNS or connection error").
- A robots.txt that redirects to a private address counts as unavailable (no robots.txt rules).
- Local runs (APIFY_IS_AT_HOME not set) are unchanged.
- Files: `my_actor/common.py` (guard helpers; `Http._robots_for` checks the site first), `my_actor/main.py` (hook
  on the httpx client, `Blocked` handled), `tests/test_ssrf_guard_1102t.py` (new), README Limits (one line).

### Unreleased

- Fixed `tags` on sites that declare tags in a `parsely-tags` meta (for example The Verge). trafilatura 2.x returns tags as a list whose element is the whole comma-separated meta value, and the old code only split plain strings, so the output was one long string with internal tracking tokens (`theverge,pagetype:story,...,stream-…`). Every tag and category value is now split on commas and semicolons, trimmed and de-duplicated (case-insensitive), and tokens containing `:`, starting with `stream-` or equal to the site name are dropped. Categories given alone (without tags) are no longer lost. Regression test: `tests/test_tags.py` with a Verge-style fixture.
- The free status row also carries `statusRow: true`.

### 1.0.4

- A run that extracts no article (every URL failed, was blocked by robots.txt or was under **Minimum words**) now writes one free status row (`recordType: "status"`, `status: "warning"`, message starting "Not charged: no article was extracted…") instead of leaving the dataset empty. It is not charged and is not written when at least one article was saved.
