Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/azure-template-upload-headers-python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@e2b/python-sdk": patch
---

Apply the request headers the API returns with a template layer-file upload link. Azure Blob Storage requires `x-ms-blob-type` on the upload request, which its signed URL cannot carry, so `COPY` instructions failed on Azure-backed clusters. GCS- and S3-backed clusters return no headers and are unaffected.
5 changes: 5 additions & 0 deletions .changeset/azure-template-upload-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"e2b": patch
---

Apply the request headers the API returns with a template layer-file upload link. Azure Blob Storage requires `x-ms-blob-type` on the upload request, which its signed URL cannot carry, so `COPY` instructions failed on Azure-backed clusters. GCS- and S3-backed clusters return no headers and are unaffected.
8 changes: 8 additions & 0 deletions packages/js-sdk/src/api/schema.gen.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions packages/js-sdk/src/template/buildApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export async function uploadFile(
fileName: string
fileContextPath: string
url: string
headers?: Record<string, string>
ignorePatterns: string[]
resolveSymlinks: boolean
gzip: boolean
Expand All @@ -128,6 +129,7 @@ export async function uploadFile(
const {
fileName,
url,
headers,
fileContextPath,
ignorePatterns,
resolveSymlinks,
Expand All @@ -154,7 +156,7 @@ export async function uploadFile(
abortOpts?.signal
)

const res = await putFileStream(url, tar.path, tar.size, signal)
const res = await putFileStream(url, tar.path, tar.size, signal, headers)

if (!res.ok) {
throw new FileUploadError(
Expand All @@ -176,7 +178,8 @@ async function putFileStream(
url: string,
filePath: string,
size: number,
signal: AbortSignal | undefined
signal: AbortSignal | undefined,
headers?: Record<string, string>
): Promise<{ ok: boolean; statusText: string }> {
// Prefer undici's fetch: it honors the explicit Content-Length on stream
// bodies on every runtime, while Deno's native fetch ignores the header and
Expand All @@ -192,7 +195,10 @@ async function putFileStream(
body: stream.Readable.toWeb(
fs.createReadStream(filePath)
) as ReadableStream,
// Headers the API asked for, applied as given (Azure's Put Blob requires
// x-ms-blob-type, which its SAS cannot carry). Content-Length stays ours.
headers: {
...headers,
'Content-Length': size.toString(),
},
// Streaming request bodies require half-duplex mode.
Expand Down
3 changes: 2 additions & 1 deletion packages/js-sdk/src/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ export class TemplateBase
stackTrace = this.stackTraces[index + 1]
}

const { present, url } = await getFileUploadLink(
const { present, url, headers } = await getFileUploadLink(
client,
{
templateID,
Expand All @@ -1131,6 +1131,7 @@ export class TemplateBase
fileName: src,
fileContextPath: this.fileContextPath.toString(),
url,
headers,
ignorePatterns: [
...this.fileIgnorePatterns,
...readDockerignore(this.fileContextPath.toString()),
Expand Down
40 changes: 40 additions & 0 deletions packages/js-sdk/tests/template/uploadFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,45 @@ describe('uploadFile transfer encoding', () => {
// Content-Type (e.g. inferred from the archive's file extension) makes
// the storage backend reject the upload with 403 Forbidden.
expect(capturedHeaders['content-type']).toBeUndefined()

// S3 and GCS presigned PUTs sign the header set, so the upload must add
// nothing the API did not ask for.
expect(capturedHeaders['x-ms-blob-type']).toBeUndefined()
})

test('sends the headers the API returned with the upload link', async () => {
await uploadFile(
{
fileName: '*.txt',
fileContextPath: testDir,
url: baseUrl,
headers: { 'x-ms-blob-type': 'BlockBlob' },
ignorePatterns: [],
resolveSymlinks: false,
gzip: true,
},
undefined
)

// Azure's Put Blob rejects the request without it, and its SAS cannot
// carry a required request header, so the API hands it back instead.
expect(capturedHeaders['x-ms-blob-type']).toBe('BlockBlob')
})

test('keeps its own Content-Length when the API returns one', async () => {
await uploadFile(
{
fileName: '*.txt',
fileContextPath: testDir,
url: baseUrl,
headers: { 'Content-Length': '1' },
ignorePatterns: [],
resolveSymlinks: false,
gzip: true,
},
undefined
)

expect(Number(capturedHeaders['content-length'])).toBe(capturedBodyLength)
})
})

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/python-sdk/e2b/api/client/models/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions packages/python-sdk/e2b/template_async/build_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import os
from types import TracebackType
from typing import Callable, Optional, List, Union
from typing import Callable, Dict, Optional, List, Union

import httpx
from pyqwest import HTTPTransport
Expand Down Expand Up @@ -115,6 +115,7 @@ async def upload_file(
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,

request_timeout: Optional[float] = None,
):
# Uploading a large build-context archive can take far longer than the 60s
Expand Down Expand Up @@ -156,10 +157,13 @@ async def upload_file(
# explicit Content-Length suppresses chunked transfer
# encoding, which S3 presigned URLs reject; reqwest keeps the
# Content-Length framing for the streamed body.
# Headers the API asked for, applied as given (Azure's Put
# Blob requires x-ms-blob-type, which its SAS cannot carry).
# Content-Length stays ours.
response = await client.put(
url,
content=aiter_io_chunks(tar_file),
headers={"Content-Length": str(size)},
headers={**(headers or {}), "Content-Length": str(size)},
)
response.raise_for_status()
finally:
Expand Down
1 change: 1 addition & 0 deletions packages/python-sdk/e2b/template_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ async def _build(
resolve_symlinks,
gzip,
stack_trace,
headers=file_info.headers.to_dict() if file_info.headers else None,
request_timeout=request_timeout,
)
if on_build_logs:
Expand Down
Loading
Loading