You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Proxy download route ignores Range; @actions/cache restores are single-stream against this server #263
@actions/cache only uses its concurrent ranged downloader when the download URL's hostname ends in .blob.core.windows.net (packages/cache/src/internal/cacheHttpClient.ts). Against this server — proxy route or presigned URL — every restore is one httpClient.get() with no Range and no keep-alive. On our deployment (Kubernetes runner pod, Scaleway Object Storage, one 320 MB entry) that is ~15 MB/s where 8 parallel 40 MB Range GETs reach 143 MB/s on the same URL. (Filed upstream on actions/toolkit as well; #1943 does not change the gate.)
Two things the server could do so a range-capable client gets full speed while credentials stay server-side:
1. Honour Range on /download/:id
routes/download/[cacheEntryId].ts streams the whole object for any request. We run this in our fork (happy to send it as a PR); it:
parses a single bytes=start-end / bytes=start- range and passes it through to the backend (GetObjectCommand({ Range }), createReadStream({ start, end }), GCS createReadStream({ start, end })) rather than slicing in the server;
answers 206 with content-range: bytes start-end/size and content-length only when the adapter served the range, 200 with content-length otherwise, and 416 with content-range: bytes */size when the range starts past the end;
leaves unmerged entries as they are (200, streamed from parts — a client keys off the status).
Adapters return { stream, size, range } instead of a bare stream, and the response is piped with stream.pipeline so a client abort destroys the backend read (h3's sendStream web-stream path neither applies backpressure nor notices the client hanging up — with a ranged client that fans out 8 requests and retries, that leaks 8 S3 GETs per abort). Tests cover closed, open-ended, clamped, unsatisfiable, malformed, and unmerged cases on the filesystem and S3 matrix.
Measured effect with a ranged client, 320 MB entry, sha256-verified: 5.6 s (single stream) → 3.5 s, transfer 140 MB/s over 9 requests.
2. Merge eagerly on FinalizeCacheEntryUpload (relates to #262)
The first restore of an entry after its save is served from parts and single-stream by construction, and only that first download kicks off the merge. For a "save on main, restore on the next PR" pattern that first restore is the common case. Merging when the upload finalizes (or shortly after, off the request path) would make the fast path the default one.
Reference
Presigned S3 URLs are signed for GET only, so HEAD answers 403; a Range: bytes=0-0 GET returns the total in content-range, which is how a client can size its ranges without HEAD.
One more, found while testing
createDownloadUrl passes expiresIn: 10 * 60 * 1000 to getSignedUrl, but the presigner takes seconds — presigned URLs are valid for ~6.9 days (just under the SigV4 cap), while the comment says 10 minutes.
Please send the PR for Range support on /download/:id. Passing the range through the adapters and answering 206, 416 or 200 works for me, and unmerged entries should keep returning 200. In review I'll mostly check the new adapter return type and the unmerged path.
The sendStream abort leak needs a fix even for clients that never send Range. If you can, put the stream.pipeline change in its own commit.
Eager merge on finalize is the same proposal as #262, which uses UploadPartCopy. Let's continue that in #262.
The presigned URL expiry is already fixed on dev. createDownloadUrl now passes Math.floor((expiresAt - Date.now()) / 1000), which is in seconds. Your fork probably predates 8d9d11e.
Shipped in #270 (Range on /download/:id) and #282 (pipe instead of sendStream). Both will be in the next release after v9.8.0. Eager merge was tracked in #262.
Activity
LouisHaftmann commentedon Sep 13, 2026
Thanks for the write-up and the measurements.
Please send the PR for Range support on
/download/:id. Passing the range through the adapters and answering 206, 416 or 200 works for me, and unmerged entries should keep returning 200. In review I'll mostly check the new adapter return type and the unmerged path.The
sendStreamabort leak needs a fix even for clients that never sendRange. If you can, put thestream.pipelinechange in its own commit.Eager merge on finalize is the same proposal as #262, which uses
UploadPartCopy. Let's continue that in #262.The presigned URL expiry is already fixed on
dev.createDownloadUrlnow passesMath.floor((expiresAt - Date.now()) / 1000), which is in seconds. Your fork probably predates 8d9d11e.LouisHaftmann commentedon Oct 23, 2026
Shipped in #270 (Range on
/download/:id) and #282 (pipe instead ofsendStream). Both will be in the next release after v9.8.0. Eager merge was tracked in #262.