diff --git a/.changeset/early-hotels-peel.md b/.changeset/early-hotels-peel.md new file mode 100644 index 0000000000..826d4ed026 --- /dev/null +++ b/.changeset/early-hotels-peel.md @@ -0,0 +1,6 @@ +--- +'@e2b/python-sdk': patch +'e2b': patch +--- + +fix: set Content-Length on presigned PUT uploads for S3 compatibility diff --git a/packages/js-sdk/src/template/buildApi.ts b/packages/js-sdk/src/template/buildApi.ts index d699efe03c..d474ea7b7b 100644 --- a/packages/js-sdk/src/template/buildApi.ts +++ b/packages/js-sdk/src/template/buildApi.ts @@ -119,12 +119,21 @@ export async function uploadFile( resolveSymlinks ) - // The compiler assumes this is Web fetch API, but it's actually Node.js fetch API + // Buffer the gzipped tar so fetch sends it with Content-Length. + // S3 presigned PUT URLs reject Transfer-Encoding: chunked (501). + // Node.js fetch (undici) auto-sets Content-Length for Buffer bodies. + let body: Buffer + { + const chunks: Buffer[] = [] + for await (const chunk of uploadStream as unknown as AsyncIterable) { + chunks.push(chunk) + } + body = Buffer.concat(chunks) + } + const res = await fetch(url, { method: 'PUT', - // @ts-expect-error - body: uploadStream, - duplex: 'half', + body, }) if (!res.ok) { diff --git a/packages/js-sdk/src/template/utils.ts b/packages/js-sdk/src/template/utils.ts index 4b9cca2acf..c0bedd82f2 100644 --- a/packages/js-sdk/src/template/utils.ts +++ b/packages/js-sdk/src/template/utils.ts @@ -385,10 +385,11 @@ export async function tarFileStream( } /** - * Create a tar stream for upload using chunked transfer encoding. + * Create a tar stream for upload. * * @param fileName Glob pattern for files to include * @param fileContextPath Base directory for resolving file paths + * @param ignorePatterns Ignore patterns to exclude from the archive * @param resolveSymlinks Whether to follow symbolic links * @returns A readable stream of the gzipped tar archive */ diff --git a/packages/python-sdk/e2b/template_async/build_api.py b/packages/python-sdk/e2b/template_async/build_api.py index 7da04b1d89..0930ff5d0a 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -107,12 +107,25 @@ async def upload_file( stack_trace: Optional[TracebackType], ): try: - tar_buffer = tar_file_stream( - file_name, context_path, ignore_patterns, resolve_symlinks + loop = asyncio.get_running_loop() + tar_buffer = await loop.run_in_executor( + None, + tar_file_stream, + file_name, + context_path, + ignore_patterns, + resolve_symlinks, ) + # Use bytes with explicit Content-Length. + # S3 presigned PUT URLs do not support Transfer-Encoding: chunked. + body = tar_buffer.getvalue() client = api_client.get_async_httpx_client() - response = await client.put(url, content=tar_buffer.getvalue()) + response = await client.put( + url, + content=body, + headers={"Content-Length": str(len(body))}, + ) response.raise_for_status() except httpx.HTTPStatusError as e: raise FileUploadException(f"Failed to upload file: {e}").with_traceback( diff --git a/packages/python-sdk/e2b/template_sync/build_api.py b/packages/python-sdk/e2b/template_sync/build_api.py index 22f0be9033..59ac5ee1aa 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -110,8 +110,16 @@ def upload_file( tar_buffer = tar_file_stream( file_name, context_path, ignore_patterns, resolve_symlinks ) + + # Use bytes with explicit Content-Length. + # S3 presigned PUT URLs do not support Transfer-Encoding: chunked. + body = tar_buffer.getvalue() client = api_client.get_httpx_client() - response = client.put(url, content=tar_buffer.getvalue()) + response = client.put( + url, + content=body, + headers={"Content-Length": str(len(body))}, + ) response.raise_for_status() except httpx.HTTPStatusError as e: raise FileUploadException(f"Failed to upload file: {e}").with_traceback(