Podcast and Newsletter Feed Enclosure Delivery Gate
Pricing
from $20.00 / 1,000 run starteds
Podcast and Newsletter Feed Enclosure Delivery Gate
Reads your RSS, Atom or JSON Feed, then sends one HEAD and one ranged GET to each recent enclosure. It reports range support, content type, declared length against reported length, the redirect chain, TLS use, and every item whose GUID or enclosure URL ch
Pricing
from $20.00 / 1,000 run starteds
Rating
0.0
(0)
Developer
kingii98
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 days ago
Last modified
Categories
Share
Prove, before you publish, that each new episode file plays for every client, and that no item identity changed by accident.
This Actor reads your RSS, Atom or JSON Feed, takes the most recent items, and
sends one HEAD and one ranged GET to each enclosure. It reports what the
media host really does: whether the server honours a Range header with 206,
which content type it sends, whether the length in the feed agrees with the size
the server reports, how long the redirect chain is, and whether every hop uses
TLS. It also keeps a small baseline, so it can tell you when the GUID or the
enclosure URL of an item changed — the change that makes every subscriber
download the episode again.
The full media file is never downloaded. The Actor reads 64 KB by default.
Who this is for
The publisher or the operations person of a podcast or a media newsletter, who must know that a new episode is deliverable before it goes out. Run it after each publication, and again after a fix.
What it does not do
- No browser, no proxy, no paid API, no LLM.
- No full media download. Only HEAD and one bounded ranged GET.
- No write to your feed or your media host.
Checks
| Check | Pass condition | Why it matters |
|---|---|---|
range_support | The ranged GET answers 206 with a Content-Range header. | Without range support a player cannot seek, and it cannot resume a broken download. |
content_type | The media type served equals the type declared in the feed. | A file served as text/html or as the wrong type is refused by strict clients. |
declared_length_match | The size reported by the server is inside lengthTolerancePercent of the length in the feed. | A wrong length breaks progress bars and some download clients. |
redirect_depth | The chain ends inside maxRedirects hops. | A client with a lower hop limit cannot reach the file. |
https_only | The enclosure URL and every hop use HTTPS. | A plain HTTP hop is blocked by App Transport Security and by mixed content rules. |
guid_stability | No item in the checked window changed its GUID or its enclosure URL against the baseline. | A changed identity re-downloads the episode for every subscriber. |
A check that cannot be decided is reported as skip, not as a failure. An
example is content_type for a feed that declares no enclosure type.
declared_length_match uses a tolerance of 1 percent by default, because
dynamic ad insertion moves the real size by a small amount. Set
lengthTolerancePercent to 0 for a strict comparison.
Input
Every field has a default, so a run with an empty input {} works.
| Field | Type | Default | Description |
|---|---|---|---|
feedUrls | array | ["https://feeds.twit.tv/twit.xml"] | 1 to 10 public RSS, Atom or JSON Feed URLs. |
itemsToCheck | integer | 5 | How many recent items of each feed to probe. Maximum 50. |
checks | array | all six | The checks that decide the gate. |
maxRedirects | integer | 5 | Hop limit for one enclosure. The feed fetch keeps its own fixed limit of 5 hops. |
probeBytes | integer | 65536 | Size of the ranged read. |
lengthTolerancePercent | integer | 1 | Allowed difference between the declared length and the reported size. |
maxEnclosures | integer | 100 | Hard cap on probed enclosures for one run. |
timeoutSeconds | integer | 20 | Timeout for one HTTP request. |
maxFeedBytes | integer | 10485760 | Hard cap on the bytes read from one feed. |
concurrency | integer | 4 | How many probes run at the same time. |
baselineStoreName | string | feed-enclosure-baseline | Named key-value store that holds the item identity history. |
updateBaseline | boolean | true | Write this run into the baseline. Turn it off for a dry run. |
userAgent | string | FeedEnclosureGate/0.1 (+https://apify.com) | Sent with every request. |
Safety
Private, loopback, link-local and reserved addresses are refused, for the feed
URL, for the enclosure URL and for every redirect hop. URLs that hold
credentials are refused. Feed XML is parsed with defusedxml, so an entity
expansion attack cannot reach the parser. Every read is bounded.
Output
The dataset holds four record types, told apart by recordType.
feed — one for each feed URL: feedStatus (OK, TARGET_REFUSED,
UNREACHABLE, UNPARSABLE), httpStatus, finalUrl, feedFormat,
itemsInFeed, itemsChecked, enclosuresFound, enclosuresProbed,
enclosuresBilled, identityChanges, guidStability, note.
enclosure — one for each probed enclosure: enclosureUrl, finalUrl,
redirectChain, redirectDepth, headStatus, rangeStatus, rangeSupported,
acceptRanges, contentType, declaredType, declaredLength,
reportedLength, contentRange, receivedBytes, tls, checks,
failedChecks, checkDetails, verdict (pass, fail or error), and
billed, which is true only when a request left the Actor for this enclosure.
identity-change — one for each item whose GUID or enclosure URL changed
against the baseline: itemKey, itemTitle, changedFields, previousGuid,
currentGuid, previousEnclosureUrl, currentEnclosureUrl, gatesPublish.
summary — one for each run: feedsRequested, feedsParsed,
feedsBlocked, enclosuresProbed, enclosuresPass, enclosuresFail,
enclosuresError, identityChanges, identityChangesGating,
enclosuresBilled, failedCheckCounts, checksEnabled, and the gate verdict
publishGatePass (also given as publish_gate_pass).
publishGatePass is true only when every feed parsed, every probed enclosure
passed every enabled check, and no identity change was found by an enabled
check.
Only an enabled check decides the gate. An identity change is always reported,
but it fails the gate only when checks holds guid_stability. The record
field gatesPublish and the summary field identityChangesGating tell you
which changes count.
A failed gate is a successful run
A failed gate, an unreachable feed and a refused target are business verdicts. They are written to the dataset and to the run status message, and the run ends SUCCEEDED. A run only ends FAILED when the input itself cannot be read.
A feed URL that this Actor cannot use — a missing scheme, a scheme that is not
HTTP or HTTPS, credentials in the URL, a loopback or a private host, a port
outside 0-65535, a malformed IPv6 literal or a host name that the IDNA codec
refuses — gives one feed record with feedStatus: "TARGET_REFUSED". The other feeds in the list
still run. An entry that is not a URL string at all is an input error.
The baseline
The guid_stability check needs history. The Actor keeps, in the named
key-value store baselineStoreName, one entry for each feed that holds the GUID
and the enclosure URL of each item in the checked window. The item is followed
by its link, then by its title, because the GUID is the value under test.
The first run of a feed writes the baseline and reports
guidStability: "baseline-written". From the second run on, a change is
reported. Use one store name for each show, so two shows do not share a history.
Pricing (pay per event)
| Event | Unit | Counted |
|---|---|---|
run-started | One Actor run | Once, after the input is read and accepted. |
feed-parsed | One feed fetched and parsed | Once for each feed that was fetched and parsed. A refused, unreachable or unparsable feed is not charged. |
enclosure-probed | One enclosure checked with HEAD and one ranged GET | Once for each enclosure that got at least one request. An enclosure above maxEnclosures, and an enclosure URL that the target guard refused before any request, are not charged. |
A weekly run over one feed and five episodes charges one run-started, one
feed-parsed and five enclosure-probed events.
Local use
uv syncuv run pytestuv run ruff check .uv run python -m feed_enclosure_gate
How to read a failure
range_supportfails on every episode — your media host or your CDN does not serve byte ranges. Seeking and resume are broken for every listener.content_typefails — the type in the feed and the type on the wire do not agree. Correct the feed, or correct the host.declared_length_matchfails by a large amount — the feed was built from a different file than the one that is served.redirect_depthfails — the prefix chain of your analytics services is too long. Remove one hop.guid_stabilityfails — a publishing tool rewrote the item identity. Restore it before the next fetch of your subscribers, or every listener downloads the episode again.