cloudflare / cloudflare/sandbox-sdk
interceptHttps outbound handler mishandles Expect: 100-continue responses
- Dominant language
- TypeScript
- Stars
- 1.1k
- Forks
- 114
- Avg merge
- 22h 42m
- Merged PRs (30d)
- 14
Description
## Describe the bug
When a Sandbox uses HTTPS interception (`interceptHttps = true`) and an outbound
handler forwards a request with `fetch(request)`, HTTP requests that use
`Expect: 100-continue` do not complete correctly.
The outbound handler receives the request and forwards it, but `fetch(request)`
returns the interim `100 Continue` response as if it were the final response.
The container then sends the request body and waits for the final upstream
response, but receives malformed HTTP framing instead.
This breaks S3/R2-style uploads from SDKs such as boto3/botocore, which send
`Expect: 100-continue` for `PutObject`.
## To Reproduce
Use a Sandbox subclass with HTTPS interception and a logging-only outbound
handler:
```ts
import {
ContainerProxy,
Sandbox as SandboxBase,
} from "@cloudflare/sandbox";
export { ContainerProxy };
export class Sandbox extends SandboxBase {
enableInternet = false;
interceptHttps = true;
}
Sandbox.outbound = async (request: Request) => {
const url = new URL(request.url);
console.log(JSON.stringify({
stage: "container->handler",
method: request.method,
url: `${url.protocol}//${url.hostname}${url.pathname}${url.search}`,
expect: request.headers.get("expect"),
contentLength: request.headers.get("content-length"),
hasBody: request.body !== null,
}));
const response = await fetch(request);
console.log(JSON.stringify({
stage: "upstream->handler",
status: response.status,
statusText: response.statusText,
}));
return response;
};
```
From inside the sandbox container, run a PUT request with a body and
Expect: 100-continue. This example intentionally uses an invalid SigV4
signature; the important part is the HTTP behavior:
```
printf '{"source":"curl","created_at":"manual"}' > /tmp/r2-put.json
curl -sv -o /tmp/r2-put-response.txt \
-X PUT "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${R2_BUCKET_NAME}/sigv4test/curl-manual-$(date +%s).json" \
-H "Expect: 100-continue" \
-H "Content-Type: application/json" \
-H "Authorization: AWS4-HMAC-SHA256 Credential=${R2_ACCESS_KEY_ID}/$(date -u +%Y%m%d)/auto/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=invalid" \
-H "x-amz-date: $(date -u +%Y%m%dT%H%M%SZ)" \
--data-binary @/tmp/r2-put.json
```
Observed curl output with interceptHttps = true:
```
< HTTP/1.1 100 Continue
< Transfer-Encoding: chunked
* We are completely uploaded and fine
* Received HTTP/0.9 when not allowed
```
Observed outbound handler logs:
```
{"stage":"container->handler","method":"PUT","expect":"100-continue","contentLength":"39","hasBody":true}
{"stage":"upstream->handler","status":100,"statusText":"Continue"}
```
The final upstream response is never observed by the handler.
## Expected behavior
The outbound/interception path should handle Expect: 100-continue as an
interim response and continue proxying the request body and final upstream
response.
For the intentionally invalid SigV4 curl request above, the expected sequence is:
```
HTTP/1.1 100 Continue
...
HTTP/1.1 400 Bad Request
```
or another final S3/R2 error response. The client should not receive malformed
framing or an HTTP/0.9 parse error.
## Control test
With the same container and same curl command, but with interception disabled:
```
export class Sandbox extends SandboxBase {
enableInternet = true;
interceptHttps = false;
}
```
curl behaves correctly:
```
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 400 Bad Request
< Content-Type: application/xml
< Server: cloudflare
```
The direct path also shows the real upstream certificate:
issuer: C=US; O=Google Trust Services; CN=WE1
The intercepted path shows the local interception certificate:
issuer: CN=Cloudflare TLS proxy-everything Intercept CA
So the failure appears specific to the Sandbox HTTPS interception/outbound path,
not curl, R2, boto3, or SigV4 signing.
## Impact
This breaks R2/S3 PutObject requests from boto3/botocore and likely any HTTP
client that uses Expect: 100-continue for uploads. In boto3, the failure
surfaces as:
http.client.BadStatusLine: 143\r\n
botocore.exceptions.ConnectionClosedError:
Connection was closed before we received a valid response from endpoint URL
## Version / environment
- @cloudflare/sandbox: 0.12.3
- Wrangler: 4.110.0
- Base image tested: docker.io/cloudflare/sandbox:0.12.3-python
- Local development with wrangler dev
- compatibility_flags: ["nodejs_compat"]
Contributor guide
Research direction
The issue names no source files or tests. Reproduce it with the provided Sandbox subclass and curl command, then trace the HTTPS interception and outbound-handler fetch path, including how interim 100 Continue responses and request bodies are forwarded. Done means the handler observes the final upstream response and the client receives valid interim and final HTTP framing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100