Skip to content

v0.7.0

Choose a tag to compare

@hdae hdae released this 05 Sep 21:12
· 9 commits to main since this release

This release makes rate limits survivable: a 429 or 503 is now retried automatically, honoring the server's Retry-After, and on the HuggingFace layer expectedBytes also acts as an upper bound so a response that overruns its declared length is cut off instead of downloaded in full. The API is additive — no function, type or error-message prefix changes and no migration is needed — but two defaults change: a 429 / 503 now waits and retries instead of throwing at once (retry: false restores the old behavior), and prefetchHfFile now reads expectedBytes from the spec (see below for the two cases that turn from success into a throw).

✨ Features

  • Retries for 429 / 503, on by default (retry on fetchBytes / prefetchUrl / fetchHfFile / fetchHfFiles / prefetchHfFile / resolveHfRevision; in the HF layer retry / onRetry travel to both the revision resolution and the file download, like init). Defaults are { statuses: [429, 503], maxRetries: 5, baseDelayMs: 1000 }. A Retry-After header wins when present — delta-seconds, or an HTTP-date (a date in the past means "now"); only a value starting with a letter is handed to Date.parse, so anything else — a malformed number such as 1.5 or +120 included — counts as no header. The header is followed as given unless you set maxDelayMs. Without the header the wait doubles from baseDelayMs (1, 2, 4, 8, 16 seconds); the exponent counts every retry, so a retry that followed a Retry-After advances it too. The cap is per wait, so a server that keeps answering Retry-After: 3600 is waited out once per retry (five hours at the default maxRetries); a server that keeps answering 503 without the header takes up to 31 seconds before the call rejects. Only GET and HEAD are retried: any other method (reachable through cache: false) throws on the first 429 / 503 as before. Waiting respects the AbortSignal passed via init (an abort rejects with signal.reason), the failed response's body is released before each wait, and onRetry is called before each wait with { url, status, attempt, delayMs, retryAfter } — a throwing listener is warned about and ignored, like onProgress. Connection errors, transfers cut off mid-body and every other status still throw on the first response. A call that joins an in-flight single-flight fetch waits out the leader's retries; its own retry / onRetry go unused, like the other joiner options.
  • When the retries run out, the error keeps its prefix. It is still fetch-cache: HTTP 429 Too Many Requests (url), with (再試行 N 回の後) appended whenever at least one retry ran — also when the final status is not 429 / 503 (a 429 that turns into a 404 reports the 404 with the suffix). Matching on the prefix keeps working; matching on the whole string will not once a retry has happened. A wait setTimeout cannot hold (longer than 2**31 - 1 ms, about 24.8 days) is not honored and not retried: that round's response comes back as it is, uncounted and without an onRetry call, while retries already made still count — pass maxDelayMs to cap the wait and retry anyway.
  • Malformed retry policies throw before any request is made: maxRetries must be a safe integer ≥ 0, baseDelayMs / maxDelayMs finite numbers ≥ 0, and every entry of statuses an integer in 400–599 (NaN would otherwise slip past the wait bound, and a maxRetries of NaN would never stop). The message starts with fetch-cache: retry.<field> は.
  • New exported types RetryPolicy and RetryContext, from both . and ./hf (the same declarations).
  • HF expectedBytes is an upper bound as well. A response that grows past the declared length is cut off at the chunk that crosses it and throws fetch-cache: 受信が申告 N バイトを超えた(M バイト以上) (url) right there, instead of being downloaded in full only to fail the exact-length check at the end (a short response still fails at the end, as before, with the familiar バイト数不一致 message). With into, an overrun is reported as this declaration violation, not as a too-small buffer. expectedBytes: 0 is a valid declaration for an empty file. The overrunning read is never reported to onProgress. On a runtime whose response.body is null the check runs after the whole body has arrived, so only the error changes, not the bytes transferred (and no progress event is emitted for it). The generic layer's expectedBytes stays an allocation hint and never a check.
  • prefetchHfFile now reads expectedBytes from the spec — as an upper bound only. A response overrunning it never becomes a cache entry (a short one is still not detected while warming; the exact-length check belongs to the read). Two cases that succeeded in 0.6.0 therefore now throw: a warming spec whose expectedBytes is smaller than the actual file while the reading spec declares no length (with the same declaration on both sides the read already failed and self-healed), and a malformed expectedBytes (negative, non-integer, or beyond the safe-integer range), which is now rejected before any request on every HF entry point with fetch-cache: expectedBytes は 0 以上の整数で指定してください: <value> (<path>) (fetchHfFile already failed on it, at the end of the download).
  • When a bad prefetch entry cannot be removed by the safety-net delete, the AggregateError names which check failed (SHA-256 mismatch or received-bytes overrun); the SHA-256 wording is unchanged.

📝 Docs

  • README gains a Rate limiting (429) and retries section, and the HuggingFace section describes the upper bound and what prefetchHfFile reads. The designs are recorded in ADR 0010 (including why no jitter is added to the waits and why there is no default cap) and ADR 0011; the contracts (what joiners inherit, cumulative waits, the missing-entry window while a self-heal refetch waits) are in limitations.

Full Changelog: v0.6.0...v0.7.0