fix(sdk): apply the upload headers the API returns with a file upload link - #1870
fix(sdk): apply the upload headers the API returns with a file upload link#1870michalsuba-e2b wants to merge 1 commit into
Conversation
… link Azure Blob Storage requires the request header x-ms-blob-type on Put Blob, which a signed URL cannot carry, so every template build with a COPY instruction failed on Azure-backed clusters. The API now returns the headers alongside the upload URL; both SDKs send them verbatim on the PUT and keep their own Content-Length. GCS- and S3-backed clusters return no headers, so their presigned PUTs go out with the same header set as before — their signatures cover the header list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: def2bd7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Package ArtifactsBuilt from 1e5d7d3. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.49.2-feat-azure-upload-headers.0.tgzCLI ( npm install ./e2b-cli-2.19.1-feat-azure-upload-headers.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.8.1-feat-azure-upload-headers.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.4.1-feat-azure-upload-headers.0.tgzPython SDK ( pip install ./e2b-2.49.1+feat.azure.upload.headers-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.10.0+feat.azure.upload.headers-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.5.0+feat.azure.upload.headers-py3-none-any.whl |
There was a problem hiding this comment.
TASTE.md review of the SDK changes (generated clients, spec, tests and changesets excluded — only buildApi.ts, template/index.ts, template_{sync,async}/build_api.py, template_{sync,async}/main.py were judged).
Checked: parity (T-1, T-2), API shape / keyword-only optionals (T-3, T-3a), generated-type boundary (T-18), timeouts & signal threading (T-46), comments/docstrings (T-69).
2 violations, both in the Python SDK:
- T-2 / T-1 — sync and async
upload_filediverge. The async mirror merges the API headers under its ownContent-Length({**(headers or {}), "Content-Length": str(size)}) and the JS side has a test for exactly that, but the sync mirror passesheadersstraight through, so an API-returnedContent-Lengthwould win. Same method, same semantics is the rule; the sync variant needs the same override and a matchingtest_upload_file_keeps_its_own_content_lengthcase. - T-3a — new optional
headersis positional. It was inserted beforerequest_timeoutwithout a*, so it both re-orders an existing optional and lets callers bind it by position. Both call sites already pass it by keyword, so a bare*costs nothing here (sync + async).
Not tied to a diff line: JS uploadFile/putFileStream correctly keep Content-Length ours and thread signal (T-46) — no issues found there. file_info.headers.to_dict() if file_info.headers else None in both main.py files relies on Unset.__bool__; it's internal plumbing so T-18 doesn't strictly apply, but if not isinstance(file_info.headers, Unset) would be the explicit spelling used elsewhere in the generated client.
| response = client.put(url, content=tar_file) | ||
| # Headers the API asked for, applied as given (Azure's Put | ||
| # Blob requires x-ms-blob-type, which its SAS cannot carry). | ||
| response = client.put(url, content=tar_file, headers=headers) |
There was a problem hiding this comment.
T-2 (sync and async mirrors share the same semantics) / T-1: the async upload_file merges these headers under its own Content-Length and the JS side tests "keeps its own Content-Length when the API returns one", but here headers is passed through verbatim, so an API-returned Content-Length would override httpx's correct one. Mirror the async form:
| response = client.put(url, content=tar_file, headers=headers) | |
| size = os.fstat(tar_file.fileno()).st_size | |
| response = client.put( | |
| url, | |
| content=tar_file, | |
| headers={**(headers or {}), "Content-Length": str(size)}, | |
| ) |
(import os needed, as in the async module)
(and add the sync counterpart of the JS keeps its own Content-Length test).
| resolve_symlinks: bool, | ||
| gzip: bool, | ||
| stack_trace: Optional[TracebackType], | ||
| headers: Optional[Dict[str, str]] = None, |
There was a problem hiding this comment.
T-3a (optionals are keyword-only, enforced by a bare *): a defaulted parameter without * is still positional — this both inserts a new positional before request_timeout and lets callers bind headers by position. Both call sites already pass it by keyword, so:
| headers: Optional[Dict[str, str]] = None, | |
| *, | |
| headers: Optional[Dict[str, str]] = None, |
| resolve_symlinks: bool, | ||
| gzip: bool, | ||
| stack_trace: Optional[TracebackType], | ||
| headers: Optional[Dict[str, str]] = None, |
There was a problem hiding this comment.
T-3a: same as the sync variant — make the new optional keyword-only.
| headers: Optional[Dict[str, str]] = None, | |
| *, | |
| headers: Optional[Dict[str, str]] = None, |
|
CI attribution, for the reviewer — and the live suites raise the evidence bar on the part that matters here: The changed upload path passes end to end against the real API, in both SDKs.
|
|
Live validation of the header-merge path against a real Azure BYOC env (miso9): |
Summary
Azure Blob Storage requires the request header
x-ms-blob-type: BlockBlobonPut Blob, and a signed URL cannot carry a required request header. Template builds with aCOPYinstruction therefore failed on Azure-backed clusters.GET /templates/{id}/files/{hash}now returns those headers next to the upload URL (e2b-dev/runtime#3634), and both SDKs apply them verbatim on the PUT while keeping their ownContent-Length.uploadFiletakes an optionalheaders,putFileStreammerges it underContent-Length, andTemplate's upload loop forwardsheadersfromgetFileUploadLink.upload_file(sync and async) takesheaders; bothmain.pyupload loops passfile_info.headers.GCS- and S3-backed clusters return no headers, so their presigned PUTs go out with exactly the header set they had before — which matters because those signatures cover the header list.
No user-facing API change:
Template.build()callers get working Azure builds and nothing new to pass.Spec pin
spec/runtime-refmoves to8cd25be70e2d9e6f9bb457c15fdad51b55bd4bdf, the head of the unmerged branch behind e2b-dev/runtime#3634. Re-point it at that PR's merge commit and re-runmake codegenbefore this leaves draft. The bump also carries the unrelated spec changes that landed on runtimemainsince the old pin (node sandbox-limit fields,409responses on some template endpoints), which is why the generated clients show more than theheadersfield.Verification
pnpm vitest run --project template tests/template/uploadFile.test.ts— 3 passed. New cases: the returned header reaches the PUT; nothing extra is sent when the API returns none; aContent-Lengthin the returned headers does not override the real one. Mutation-verified (drop the merge, the header test fails).uv run pytest tests/sync/template_sync/test_upload_file.py tests/async/template_async/test_upload_file.py— 14 passed, same two new cases in each. Mutation-verified in both.pnpm run lint,pnpm run typecheck,pnpm run format,uv run make lint,uv run make typecheck— clean.--project templatesuite and a real Azure build. Both need credentials and a deployed Azure environment.Sponsor: @michalsuba-e2b
🤖 Generated with Claude Code