Skip to content

fix(sdk): apply the upload headers the API returns with a file upload link - #1870

Draft
michalsuba-e2b wants to merge 1 commit into
mainfrom
feat/azure-upload-headers
Draft

fix(sdk): apply the upload headers the API returns with a file upload link#1870
michalsuba-e2b wants to merge 1 commit into
mainfrom
feat/azure-upload-headers

Conversation

@michalsuba-e2b

Copy link
Copy Markdown

Summary

Azure Blob Storage requires the request header x-ms-blob-type: BlockBlob on Put Blob, and a signed URL cannot carry a required request header. Template builds with a COPY instruction 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 own Content-Length.

  • JSuploadFile takes an optional headers, putFileStream merges it under Content-Length, and Template's upload loop forwards headers from getFileUploadLink.
  • Pythonupload_file (sync and async) takes headers; both main.py upload loops pass file_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-ref moves to 8cd25be70e2d9e6f9bb457c15fdad51b55bd4bdf, the head of the unmerged branch behind e2b-dev/runtime#3634. Re-point it at that PR's merge commit and re-run make codegen before this leaves draft. The bump also carries the unrelated spec changes that landed on runtime main since the old pin (node sandbox-limit fields, 409 responses on some template endpoints), which is why the generated clients show more than the headers field.

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; a Content-Length in 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.
  • Not run: the live --project template suite and a real Azure build. Both need credentials and a deployed Azure environment.

Sponsor: @michalsuba-e2b

🤖 Generated with Claude Code

… 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>
@cla-bot cla-bot Bot added the cla-signed label Sep 11, 2026
@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: def2bd7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@e2b/python-sdk Patch
e2b Patch

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

@github-actions

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 1e5d7d3. Download artifacts from this workflow run.

JS SDK (e2b@2.49.2-feat-azure-upload-headers.0):

npm install ./e2b-2.49.2-feat-azure-upload-headers.0.tgz

CLI (@e2b/cli@2.19.1-feat-azure-upload-headers.0):

npm install ./e2b-cli-2.19.1-feat-azure-upload-headers.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.8.1-feat-azure-upload-headers.0):

npm install ./e2b-code-interpreter-2.8.1-feat-azure-upload-headers.0.tgz

Desktop JS SDK (@e2b/desktop@2.4.1-feat-azure-upload-headers.0):

npm install ./e2b-desktop-2.4.1-feat-azure-upload-headers.0.tgz

Python SDK (e2b==2.49.1+feat.azure.upload.headers):

pip install ./e2b-2.49.1+feat.azure.upload.headers-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.10.0+feat.azure.upload.headers):

pip install ./e2b_code_interpreter-2.10.0+feat.azure.upload.headers-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.5.0+feat.azure.upload.headers):

pip install ./e2b_desktop-2.5.0+feat.azure.upload.headers-py3-none-any.whl

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. T-2 / T-1 — sync and async upload_file diverge. The async mirror merges the API headers under its own Content-Length ({**(headers or {}), "Content-Length": str(size)}) and the JS side has a test for exactly that, but the sync mirror passes headers straight through, so an API-returned Content-Length would win. Same method, same semantics is the rule; the sync variant needs the same override and a matching test_upload_file_keeps_its_own_content_length case.
  2. T-3a — new optional headers is positional. It was inserted before request_timeout without 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-3a: same as the sync variant — make the new optional keyword-only.

Suggested change
headers: Optional[Dict[str, str]] = None,
*,
headers: Optional[Dict[str, str]] = None,

@michalsuba-e2b

Copy link
Copy Markdown
Author

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. tests/template/build.test.ts (4 tests, 98.7s) exercises .copy(..., { forceUpload: true }) through the new putFileStream and is green on node, bun, deno and cloudflare, production and staging. test_build_template_with_symlinks and test_build_template_with_resolve_symlinks do the same through upload_file and pass in both sync and async. Generated files passes, so the spec/runtime-ref bump and make codegen output agree. GCS returns no headers, which is exactly the regression vector.

Python SDK (4 jobs) — one failure, test_build_template_with_skip_cache, Timeout (>180.0s) from pytest-timeout (1 failed / 1174 passed). Its template is .skip_cache().from_image("ubuntu:22.04") with no COPY, so the upload loop skips it and upload_file is never called. A forced uncached rebuild is the slowest test in the suite.

Code Interpreter JS SDK - nodetests/cwd.test.ts > cwd r, SandboxError: 500 Internal Server Error after 38s against the live API. Code-interpreter does not touch template file upload.

@michalsuba-e2b

Copy link
Copy Markdown
Author

Live validation of the header-merge path against a real Azure BYOC env (miso9): uploadFile from this branch with server-returned headers → 201 + blob landed; with headers absent → FileUploadError (Azure MissingRequiredHeader). Details: https://linear.app/e2b/issue/BYOC-228#comment-259c9df2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant