@cloudflare/sandbox 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ProcessInfoResult, A as RequestConfig, B as FileChunk, C as WriteFileRequest, D as ContainerStub, E as BaseApiResponse, F as BucketProvider, G as ListFilesOptions, H as FileStreamEvent, I as ExecEvent, J as PortCloseResult, K as LogEvent, L as ExecOptions, M as SessionRequest, N as BaseExecOptions, O as ErrorResponse, P as BucketCredentials, Q as ProcessCleanupResult, R as ExecResult, S as ReadFileRequest, T as ExecuteResponse, U as GitCheckoutResult, V as FileMetadata, W as ISandbox, X as PortListResult, Y as PortExposeResult, Z as Process, _ as GitCheckoutRequest, _t as ExecutionResult, a as CreateSessionRequest, at as ProcessStatus, b as FileOperationRequest, c as DeleteSessionResponse, ct as StreamOptions, d as ProcessClient, dt as isExecResult, et as ProcessKillResult, f as ExposePortRequest, ft as isProcess, g as InterpreterClient, gt as Execution, h as ExecutionCallbacks, ht as CreateContextOptions, i as CommandsResponse, it as ProcessStartResult, j as ResponseHandler, k as HttpClientOptions, l as PingResponse, lt as WaitForLogResult, m as UnexposePortRequest, mt as CodeContext, n as getSandbox, nt as ProcessLogsResult, o as CreateSessionResponse, ot as SandboxOptions, p as PortClient, pt as isProcessStatus, q as MountBucketOptions, r as SandboxClient, rt as ProcessOptions, s as DeleteSessionRequest, st as SessionOptions, t as Sandbox, tt as ProcessListResult, u as UtilityClient, ut as WaitForPortOptions, v as GitClient, vt as RunCodeOptions, w as CommandClient, x as MkdirRequest, y as FileClient, z as ExecutionSession } from "./sandbox-C9WRqWBO.js";
1
+ import { $ as ProcessInfoResult, A as RequestConfig, B as FileChunk, C as WriteFileRequest, D as ContainerStub, E as BaseApiResponse, F as BucketProvider, G as ListFilesOptions, H as FileStreamEvent, I as ExecEvent, J as PortCloseResult, K as LogEvent, L as ExecOptions, M as SessionRequest, N as BaseExecOptions, O as ErrorResponse, P as BucketCredentials, Q as ProcessCleanupResult, R as ExecResult, S as ReadFileRequest, T as ExecuteResponse, U as GitCheckoutResult, V as FileMetadata, W as ISandbox, X as PortListResult, Y as PortExposeResult, Z as Process, _ as GitCheckoutRequest, _t as ExecutionResult, a as CreateSessionRequest, at as ProcessStatus, b as FileOperationRequest, c as DeleteSessionResponse, ct as StreamOptions, d as ProcessClient, dt as isExecResult, et as ProcessKillResult, f as ExposePortRequest, ft as isProcess, g as InterpreterClient, gt as Execution, h as ExecutionCallbacks, ht as CreateContextOptions, i as CommandsResponse, it as ProcessStartResult, j as ResponseHandler, k as HttpClientOptions, l as PingResponse, lt as WaitForLogResult, m as UnexposePortRequest, mt as CodeContext, n as getSandbox, nt as ProcessLogsResult, o as CreateSessionResponse, ot as SandboxOptions, p as PortClient, pt as isProcessStatus, q as MountBucketOptions, r as SandboxClient, rt as ProcessOptions, s as DeleteSessionRequest, st as SessionOptions, t as Sandbox, tt as ProcessListResult, u as UtilityClient, ut as WaitForPortOptions, v as GitClient, vt as RunCodeOptions, w as CommandClient, x as MkdirRequest, y as FileClient, z as ExecutionSession } from "./sandbox-09Ce7yli.js";
2
2
  import { a as OperationType, i as ErrorResponse$1, n as ProcessExitedBeforeReadyContext, o as ErrorCode, r as ProcessReadyTimeoutContext } from "./contexts-CdrlvHWK.js";
3
3
 
4
4
  //#region ../shared/dist/request-types.d.ts
package/dist/index.js CHANGED
@@ -586,14 +586,14 @@ var BaseHttpClient = class {
586
586
  }
587
587
  /**
588
588
  * Core HTTP request method with automatic retry for container startup delays
589
- * Retries both 503 (provisioning) and 500 (startup failure) errors when they're container-related
589
+ * Retries on 503 (Service Unavailable) which indicates container is starting
590
590
  */
591
591
  async doFetch(path, options) {
592
592
  const startTime = Date.now();
593
593
  let attempt = 0;
594
594
  while (true) {
595
595
  const response = await this.executeFetch(path, options);
596
- if (await this.isRetryableContainerError(response)) {
596
+ if (this.isRetryableContainerError(response)) {
597
597
  const elapsed = Date.now() - startTime;
598
598
  const remaining = TIMEOUT_MS - elapsed;
599
599
  if (remaining > MIN_TIME_FOR_RETRY_MS) {
@@ -706,51 +706,19 @@ var BaseHttpClient = class {
706
706
  }
707
707
  /**
708
708
  * Check if response indicates a retryable container error
709
- * Uses fail-safe strategy: only retry known transient errors
710
709
  *
711
- * TODO: This relies on string matching error messages, which is brittle.
712
- * Ideally, the container API should return structured errors with a
713
- * `retryable: boolean` field to avoid coupling to error message format.
710
+ * The Sandbox DO returns proper HTTP status codes:
711
+ * - 503 Service Unavailable: Transient errors (container starting, port not ready)
712
+ * - 500 Internal Server Error: Permanent errors (bad config, missing image)
713
+ *
714
+ * We only retry on 503, which indicates the container is starting up.
715
+ * The Retry-After header suggests how long to wait.
714
716
  *
715
717
  * @param response - HTTP response to check
716
- * @returns true if error is retryable container error, false otherwise
718
+ * @returns true if error is retryable (503), false otherwise
717
719
  */
718
- async isRetryableContainerError(response) {
719
- if (response.status !== 500 && response.status !== 503) return false;
720
- try {
721
- const text = await response.clone().text();
722
- const textLower = text.toLowerCase();
723
- if ([
724
- "no such image",
725
- "container already exists",
726
- "malformed containerinspect"
727
- ].some((err) => textLower.includes(err))) {
728
- this.logger.debug("Detected permanent error, not retrying", { text });
729
- return false;
730
- }
731
- const shouldRetry = [
732
- "no container instance available",
733
- "currently provisioning",
734
- "container port not found",
735
- "connection refused: container port",
736
- "the container is not listening",
737
- "failed to verify port",
738
- "container did not start",
739
- "network connection lost",
740
- "container suddenly disconnected",
741
- "monitor failed to find container",
742
- "timed out",
743
- "timeout"
744
- ].some((err) => textLower.includes(err));
745
- if (!shouldRetry) this.logger.debug("Unknown error pattern, not retrying", {
746
- status: response.status,
747
- text: text.substring(0, 200)
748
- });
749
- return shouldRetry;
750
- } catch (error) {
751
- this.logger.error("Error checking if response is retryable", error instanceof Error ? error : new Error(String(error)));
752
- return false;
753
- }
720
+ isRetryableContainerError(response) {
721
+ return response.status === 503;
754
722
  }
755
723
  async executeFetch(path, options) {
756
724
  const url = this.options.stub ? `http://localhost:${this.options.port}${path}` : `${this.baseUrl}${path}`;
@@ -2019,7 +1987,7 @@ function resolveS3fsOptions(provider, userOptions) {
2019
1987
  * This file is auto-updated by .github/changeset-version.ts during releases
2020
1988
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
2021
1989
  */
2022
- const SDK_VERSION = "0.6.5";
1990
+ const SDK_VERSION = "0.6.6";
2023
1991
 
2024
1992
  //#endregion
2025
1993
  //#region src/sandbox.ts
@@ -2379,18 +2347,57 @@ var Sandbox = class extends Container {
2379
2347
  status: 503,
2380
2348
  headers: { "Retry-After": "10" }
2381
2349
  });
2382
- this.logger.error("Container startup failed", e instanceof Error ? e : new Error(String(e)));
2350
+ if (this.isTransientStartupError(e)) {
2351
+ this.logger.debug("Transient container startup error, returning 503", { error: e instanceof Error ? e.message : String(e) });
2352
+ return new Response("Container is starting. Please retry in a moment.", {
2353
+ status: 503,
2354
+ headers: { "Retry-After": "3" }
2355
+ });
2356
+ }
2357
+ this.logger.error("Container startup failed with permanent error", e instanceof Error ? e : new Error(String(e)));
2383
2358
  return new Response(`Failed to start container: ${e instanceof Error ? e.message : String(e)}`, { status: 500 });
2384
2359
  }
2385
2360
  return await super.containerFetch(requestOrUrl, portOrInit, portParam);
2386
2361
  }
2387
2362
  /**
2388
2363
  * Helper: Check if error is "no container instance available"
2364
+ * This indicates the container VM is still being provisioned.
2389
2365
  */
2390
2366
  isNoInstanceError(error) {
2391
2367
  return error instanceof Error && error.message.toLowerCase().includes("no container instance");
2392
2368
  }
2393
2369
  /**
2370
+ * Helper: Check if error is a transient startup error that should trigger retry
2371
+ *
2372
+ * These errors occur during normal container startup and are recoverable:
2373
+ * - Port not yet mapped (container starting, app not listening yet)
2374
+ * - Connection refused (port mapped but app not ready)
2375
+ * - Timeouts during startup (recoverable with retry)
2376
+ * - Network transients (temporary connectivity issues)
2377
+ *
2378
+ * Errors NOT included (permanent failures):
2379
+ * - "no such image" - missing Docker image
2380
+ * - "container already exists" - name collision
2381
+ * - Configuration errors
2382
+ */
2383
+ isTransientStartupError(error) {
2384
+ if (!(error instanceof Error)) return false;
2385
+ const msg = error.message.toLowerCase();
2386
+ return [
2387
+ "container port not found",
2388
+ "connection refused: container port",
2389
+ "the container is not listening",
2390
+ "failed to verify port",
2391
+ "container did not start",
2392
+ "network connection lost",
2393
+ "container suddenly disconnected",
2394
+ "monitor failed to find container",
2395
+ "timed out",
2396
+ "timeout",
2397
+ "the operation was aborted"
2398
+ ].some((pattern) => msg.includes(pattern));
2399
+ }
2400
+ /**
2394
2401
  * Helper: Parse containerFetch arguments (supports multiple signatures)
2395
2402
  */
2396
2403
  parseContainerFetchArgs(requestOrUrl, portOrInit, portParam) {
@@ -2606,6 +2613,9 @@ var Sandbox = class extends Container {
2606
2613
  },
2607
2614
  waitForPort: async (port, options) => {
2608
2615
  await this.waitForPortReady(data.id, data.command, port, options);
2616
+ },
2617
+ waitForExit: async (timeout) => {
2618
+ return this.waitForProcessExit(data.id, data.command, timeout);
2609
2619
  }
2610
2620
  };
2611
2621
  }
@@ -2731,6 +2741,30 @@ var Sandbox = class extends Container {
2731
2741
  }
2732
2742
  }
2733
2743
  /**
2744
+ * Wait for a process to exit
2745
+ * Returns the exit code
2746
+ */
2747
+ async waitForProcessExit(processId, command, timeout) {
2748
+ const stream = await this.streamProcessLogs(processId);
2749
+ let timeoutId;
2750
+ let timeoutPromise;
2751
+ if (timeout !== void 0) timeoutPromise = new Promise((_, reject) => {
2752
+ timeoutId = setTimeout(() => {
2753
+ reject(this.createReadyTimeoutError(processId, command, "process exit", timeout));
2754
+ }, timeout);
2755
+ });
2756
+ try {
2757
+ const streamProcessor = async () => {
2758
+ for await (const event of parseSSEStream(stream)) if (event.type === "exit") return { exitCode: event.exitCode ?? 1 };
2759
+ throw new Error(`Process ${processId} stream ended unexpectedly without exit event`);
2760
+ };
2761
+ if (timeoutPromise) return await Promise.race([streamProcessor(), timeoutPromise]);
2762
+ return await streamProcessor();
2763
+ } finally {
2764
+ if (timeoutId) clearTimeout(timeoutId);
2765
+ }
2766
+ }
2767
+ /**
2734
2768
  * Match a pattern against text
2735
2769
  */
2736
2770
  matchPattern(text, pattern) {