feat: data-links - recover from expired signing credentials during large data-link uploads - #652
feat: data-links - recover from expired signing credentials during large data-link uploads#652georgi-seqera wants to merge 3 commits into
Conversation
…rge data-link uploads
…ndpoint
Re-signing presigned upload URLs is now a mode of the regular data-link
upload endpoint (POST /data-links/{id}/upload with uploadId + partNumbers)
rather than a dedicated /upload/refresh endpoint. Adopt that on the client:
- refreshUrls() calls generateDataLinkUploadUrl[WithPath] and zips the
positional response back to the requested part numbers.
- Detect an old Platform (that ignores the fields and initiates a new upload)
by the echoed uploadId no longer matching, and fail with a clear message.
- Drop the removed DataLinkRefreshMultiPartUpload*/PartUploadUrl models.
- Bump tower-java-sdk 1.192.0 -> 1.194.0 (generated from the updated spec).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| "methods":[{"name":"<init>","parameterTypes":[] }, {"name":"addPartNumbersItem","parameterTypes":["java.lang.Integer"] }, {"name":"contentLength","parameterTypes":["java.lang.Long"] }, {"name":"contentType","parameterTypes":["java.lang.String"] }, {"name":"dirPath","parameterTypes":["java.lang.String"] }, {"name":"equals","parameterTypes":["java.lang.Object"] }, {"name":"fileName","parameterTypes":["java.lang.String"] }, {"name":"getContentLength","parameterTypes":[] }, {"name":"getContentType","parameterTypes":[] }, {"name":"getDirPath","parameterTypes":[] }, {"name":"getFileName","parameterTypes":[] }, {"name":"getPartNumbers","parameterTypes":[] }, {"name":"getUploadId","parameterTypes":[] }, {"name":"hashCode","parameterTypes":[] }, {"name":"partNumbers","parameterTypes":["java.util.List"] }, {"name":"setContentLength","parameterTypes":["java.lang.Long"] }, {"name":"setContentType","parameterTypes":["java.lang.String"] }, {"name":"setDirPath","parameterTypes":["java.lang.String"] }, {"name":"setFileName","parameterTypes":["java.lang.String"] }, {"name":"setPartNumbers","parameterTypes":["java.util.List"] }, {"name":"setUploadId","parameterTypes":["java.lang.String"] }, {"name":"toIndentedString","parameterTypes":["java.lang.Object"] }, {"name":"toString","parameterTypes":[] }, {"name":"uploadId","parameterTypes":["java.lang.String"] }] | ||
| }, | ||
| { | ||
| "name":"io.seqera.tower.model.DataLinkRefreshMultiPartUploadResponse", |
There was a problem hiding this comment.
I think this does not exist anymore, right?
| if (statusCode == 500 || statusCode == 502 || statusCode == 503 || statusCode == 504) { | ||
| return UploadErrorType.TRANSIENT; | ||
| } | ||
| return UploadErrorType.HARD_FAIL; |
There was a problem hiding this comment.
Should we maybe also remove 429 from the HARD_FAIL? It seems to me that it can be TRANSIENT as it could be returned for throttling and we might miss those cases. Wdyt?
sabulous
left a comment
There was a problem hiding this comment.
Looks good to me overall!
Here are my testing notes: built the branch against a locally-generated 1.196.0 SDK and ran it against a local Platform on #11943 — the full multipart happy path works end-to-end (initiate → parts → finish). I could not exercise the refresh path live, though: presigned URLs carry a 24h TTL (X-Amz-Expires=86400), so a normal upload finishes long before credentials expire and the re-sign never triggers. Flagging in case anyone has a way to force a real mid-upload expiry.
| // A Platform that predates re-signing ignores the uploadId/partNumbers fields and instead initiates a | ||
| // brand-new multi-part upload, returning a different uploadId. Detect that by the echoed uploadId and | ||
| // fail clearly rather than mixing URLs from a different upload into the in-progress one. | ||
| if (!uploadId.equals(response.getUploadId())) { |
There was a problem hiding this comment.
Not a blocker but worth noting I think: On a Platform that predates re-signing, generateDataLinkUploadUrl with partNumbers will initiate a new multipart upload server-side (new uploadId) before we detect the mismatch at line 231 and throw. The original upload gets aborted by the caller's catch, but that newly-created one is left in-progress on S3 (billable until a lifecycle rule reaps it). Only affects the old-Platform fallback path. Should we clean that orphan upload as well or is it not necessary?
There was a problem hiding this comment.
Good point! Will update to clean up and finalize/cancel the uploads.
@sabulous Thanks for testing! I tested the real refresh scenario with steps described on the corresponding platform change https://github.com/seqeralabs/platform/pull/11943#issuecomment-5189733165 with a local Platform instance where with the |
Thanks @georgi-seqera , I could set it up and observe the second
Edit: One last thing, maybe it's worth exploring if we can communicate with the user through terminal that we are refreshing the creds. Totally optional tho. |

Summary
Recover from expired signing credentials during large data-link uploads
Why
tw data-links uploadfails partway through large uploads withHTTP 400 ExpiredToken(e.g. a 108 GiB upload died at ~75% after ~60 minutes). The presigned URLs the CLI receives at the start of a multipart upload are signed with temporary credentials that expire after ~1 hour, and the CLI had no way to recover — it uploaded parts strictly sequentially, with no retry and no ability to obtain fresh URLs, so any upload outliving the credential lifetime failed outright.This is the client half of the fix; it pairs with the new Platform refresh endpoint introduced in
26.2.0(API version1.192.0).How
The CLI now detects an expired-credentials error mid-upload, asks Platform to re-sign the URLs for the parts it still needs, and retries them — so a long-running upload transparently continues instead of failing.
Alongside that, the upload path gains general resilience it was missing:
404), the CLI stops with a clear, actionable message rather than a confusing low-level error.Behavior differs per provider, matching what each backend can actually support:
Consuming the refresh endpoint requires a regenerated SDK, so this PR also bumps the Tower Java SDK version.
What it looks like
No new flags or usage changes — the same command now survives credential expiry:
Against a Platform too old to support refresh, it fails fast with a clear reason instead of a raw
ExpiredToken:Scope