@blaxel/core 0.3.9-preview.237 → 0.3.9-preview.239

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.
@@ -24,8 +24,8 @@ function missingCredentialsMessage() {
24
24
  return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
25
25
  }
26
26
  // Build info - these placeholders are replaced at build time by build:replace-imports
27
- const BUILD_VERSION = "0.3.9-preview.237";
28
- const BUILD_COMMIT = "8ec1775f74429ef5122d03f6b708a122c87ef880";
27
+ const BUILD_VERSION = "0.3.9-preview.239";
28
+ const BUILD_COMMIT = "73d9ca5549cb452d2440ec2a738f2718cc0f065d";
29
29
  const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
30
30
  const BLAXEL_API_VERSION = "2026-04-28";
31
31
  // Bun < 1.3.11 never sends connection-level WINDOW_UPDATE: the pooled h2
@@ -17,6 +17,14 @@ const TRANSIENT_RESET_MARKERS = [
17
17
  "HTTP/2 session closed before response", // thrown by our own h2 transport
18
18
  "HTTP/2 session sent GOAWAY before response",
19
19
  ];
20
+ // HTTP statuses the edge/CDN (e.g. CloudFront) returns when it — not the
21
+ // sandbox — fails to get a usable response from origin: 502 Bad Gateway,
22
+ // 503 Service Unavailable, 504 Gateway Timeout. On an IDEMPOTENT request these
23
+ // are safe to retry (the sandbox is likely waking from standby, or the edge
24
+ // briefly could not reach origin). Retried with a small, separate budget from
25
+ // transport resets because each attempt can itself burn the edge's ~60s
26
+ // origin-read timeout before failing.
27
+ export const GATEWAY_ERROR_STATUSES = new Set([502, 503, 504]);
20
28
  // Node-level error codes (from `error.code` / `error.cause.code`) that mean
21
29
  // the connection itself dropped mid-flight and the request never completed.
22
30
  // These are safe to retry for an idempotent request.
@@ -51,19 +59,36 @@ function collectErrorText(error) {
51
59
  // like "GOAWAY" or "ERR_HTTP2". Guarding on this stops a marker-bearing 4xx/5xx
52
60
  // body from being misread as transient and retried (the over-match Codex flagged
53
61
  // for the now default-on idempotent-read retry).
54
- function hasHttpResponseStatus(error) {
62
+ function getHttpResponseStatus(error) {
55
63
  let current = error;
56
64
  for (let depth = 0; depth < 5 && current && typeof current === "object"; depth++) {
57
65
  const node = current;
58
66
  if (typeof node.status === "number")
59
- return true;
60
- if (node.response && typeof node.response === "object" &&
61
- typeof node.response.status === "number") {
62
- return true;
67
+ return node.status;
68
+ if (node.response && typeof node.response === "object") {
69
+ const responseStatus = node.response.status;
70
+ if (typeof responseStatus === "number")
71
+ return responseStatus;
63
72
  }
64
73
  current = node.cause;
65
74
  }
66
- return false;
75
+ return null;
76
+ }
77
+ function hasHttpResponseStatus(error) {
78
+ return getHttpResponseStatus(error) !== null;
79
+ }
80
+ /**
81
+ * True when the error carries an edge gateway status (502/503/504). Unlike a
82
+ * transport reset (see isTransientResetError), this DOES carry an HTTP status —
83
+ * it is the edge failing to reach origin, not the sandbox returning an
84
+ * application error — so it is retried on idempotent operations with its own
85
+ * small budget.
86
+ */
87
+ export function isRetryableGatewayError(error) {
88
+ if (!error || typeof error !== "object")
89
+ return false;
90
+ const status = getHttpResponseStatus(error);
91
+ return status !== null && GATEWAY_ERROR_STATUSES.has(status);
67
92
  }
68
93
  /**
69
94
  * True only for transport-level resets/drops that are safe to retry on an
@@ -99,6 +124,11 @@ export function isTransientResetError(error) {
99
124
  }
100
125
  const DEFAULT_BASE_DELAY_MS = 200;
101
126
  const DEFAULT_MAX_DELAY_MS = 2000;
127
+ // Gateway (502/503/504) retries get their own small budget: each attempt can
128
+ // burn the edge's ~60s origin-read timeout before failing, so a large budget
129
+ // would stall a caller for minutes. Two extra attempts is enough to ride out a
130
+ // standby wake without turning a hard outage into a multi-minute hang.
131
+ const DEFAULT_GATEWAY_RETRIES = 2;
102
132
  // Exponential backoff with full-jitter on top of one base delay, capped so a
103
133
  // single wait never blocks unreasonably long. Exponential (rather than linear)
104
134
  // gives a later attempt room to span a multi-second sandbox cold-start/standby
@@ -118,20 +148,38 @@ export function backoffDelayMs(attempt, baseDelayMs, maxDelayMs) {
118
148
  * PUT of the same bytes) — never a non-idempotent POST such as process.exec,
119
149
  * which would duplicate the side effect (ENG-2340).
120
150
  *
151
+ * Also retries edge gateway statuses (502/503/504) on their own small budget
152
+ * (`gatewayRetries`, default 2): these carry an HTTP status but come from the
153
+ * edge failing to reach origin (a standby wake, not an application error), so
154
+ * they are safe to retry on an idempotent request.
155
+ *
121
156
  * Defaults to `settings.sandboxReadRetries` (the higher idempotent-read budget,
122
157
  * sized for a multi-second standby wake). The upload path passes
123
158
  * `{ retries: settings.fsPartRetries }` to keep its own (lower) budget.
124
159
  */
125
160
  export async function retryOnTransientReset(fn, options = {}) {
126
161
  const retries = options.retries ?? settings.sandboxReadRetries;
162
+ const gatewayRetries = options.gatewayRetries ?? DEFAULT_GATEWAY_RETRIES;
127
163
  const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
128
164
  const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
129
165
  let attempt = 0;
166
+ let gatewayAttempt = 0;
130
167
  for (;;) {
131
168
  try {
132
169
  return await fn();
133
170
  }
134
171
  catch (error) {
172
+ // Edge gateway timeouts carry an HTTP status, so they are handled here
173
+ // rather than by the transport-reset branch (which ignores anything with
174
+ // a status), on their own smaller budget.
175
+ if (isRetryableGatewayError(error)) {
176
+ gatewayAttempt++;
177
+ if (gatewayRetries <= 0 || gatewayAttempt > gatewayRetries) {
178
+ throw error;
179
+ }
180
+ await new Promise((resolve) => setTimeout(resolve, backoffDelayMs(gatewayAttempt, baseDelayMs, maxDelayMs)));
181
+ continue;
182
+ }
135
183
  attempt++;
136
184
  if (retries <= 0 || attempt > retries || !isTransientResetError(error)) {
137
185
  throw error;
@@ -5,31 +5,85 @@ import { createPoolBackedH2Fetch, h2RequestDirectFromPool } from "../common/h2fe
5
5
  import { h2Pool } from "../common/h2pool.js";
6
6
  import { getForcedUrl, getGlobalUniqueHash } from "../common/internal.js";
7
7
  import { settings } from "../common/settings.js";
8
+ import { GATEWAY_ERROR_STATUSES } from "../common/transient-retry.js";
8
9
  import { client as defaultClient } from "./client/client.gen.js";
10
+ const GATEWAY_STATUS_TEXT = {
11
+ 502: "Bad Gateway",
12
+ 503: "Gateway Service Unavailable",
13
+ 504: "Gateway Timeout",
14
+ };
15
+ // Pull a short, human-readable detail out of the response payload, if any.
16
+ function extractErrorDetail(data, error) {
17
+ const fromError = error && typeof error === "object" && "error" in error
18
+ ? error.error
19
+ : undefined;
20
+ const fromData = data && typeof data === "object" && "error" in data
21
+ ? data.error
22
+ : undefined;
23
+ const detail = fromError ?? fromData;
24
+ if (detail === undefined || detail === null)
25
+ return undefined;
26
+ if (typeof detail === "string")
27
+ return detail;
28
+ if (typeof detail === "number" || typeof detail === "boolean")
29
+ return String(detail);
30
+ try {
31
+ return JSON.stringify(detail);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
9
37
  export class ResponseError extends Error {
10
38
  response;
11
- data;
12
39
  error;
40
+ /** HTTP status code of the failing response, if any. */
41
+ status;
42
+ statusText;
43
+ /** Parsed response body, when the client managed to parse one. */
44
+ data;
13
45
  constructor(response, data, error) {
14
- let dataError = {};
15
- if (data && typeof data === 'object' && 'error' in data) {
16
- dataError = data;
17
- }
18
- if (error && typeof error === 'object' && 'error' in error) {
19
- dataError['error'] = error.error;
20
- }
21
- if (response.status) {
22
- dataError['status'] = response.status;
23
- }
24
- if (response.statusText) {
25
- dataError['statusText'] = response.statusText;
26
- }
27
- super(JSON.stringify(dataError));
46
+ const status = response.status || undefined;
47
+ const statusText = response.statusText || undefined;
48
+ const detail = extractErrorDetail(data, error);
49
+ const label = status !== undefined ? String(status) : "unknown";
50
+ const suffix = statusText ? ` ${statusText}` : "";
51
+ const message = `Sandbox request failed with status ${label}${suffix}${detail ? `: ${detail}` : ""}`;
52
+ super(message);
28
53
  this.response = response;
29
- this.data = data;
30
54
  this.error = error;
55
+ this.name = "ResponseError";
56
+ this.status = status;
57
+ this.statusText = statusText;
58
+ this.data = data;
59
+ }
60
+ }
61
+ /**
62
+ * Thrown when the edge/CDN in front of the sandbox returns a gateway status
63
+ * (502/503/504) — the request never got a usable answer from the sandbox
64
+ * itself (it is waking from standby, the command outran the edge's ~60s
65
+ * origin-read timeout, or the edge could not reach origin). Safe to retry on an
66
+ * idempotent operation. Catch it with `err instanceof SandboxGatewayError` or
67
+ * `isGatewayTimeout(err)`.
68
+ */
69
+ export class SandboxGatewayError extends ResponseError {
70
+ constructor(response, data, error) {
71
+ super(response, data, error);
72
+ this.name = "SandboxGatewayError";
73
+ const known = this.status !== undefined ? GATEWAY_STATUS_TEXT[this.status] : undefined;
74
+ this.message = `Sandbox unreachable at the edge gateway (${this.status ?? "unknown"}${known ? ` ${known}` : ""}). The sandbox may be waking from standby or the request outran the edge timeout; retry an idempotent request or poll the sandbox.`;
31
75
  }
32
76
  }
77
+ /** True when `err` is a gateway status (502/503/504) from the edge. */
78
+ export function isGatewayError(err) {
79
+ return (err instanceof ResponseError &&
80
+ err.status !== undefined &&
81
+ GATEWAY_ERROR_STATUSES.has(err.status));
82
+ }
83
+ /** True when `err` is specifically an edge gateway timeout (504). */
84
+ export function isGatewayTimeout(err) {
85
+ return err instanceof ResponseError && err.status === 504;
86
+ }
33
87
  export class SandboxAction {
34
88
  sandbox;
35
89
  _h2Client = null;
@@ -119,6 +173,9 @@ export class SandboxAction {
119
173
  }
120
174
  handleResponseError(response, data, error) {
121
175
  if (!response.ok || !data) {
176
+ if (GATEWAY_ERROR_STATUSES.has(response.status)) {
177
+ throw new SandboxGatewayError(response, data, error);
178
+ }
122
179
  throw new ResponseError(response, data, error);
123
180
  }
124
181
  }
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  /* Export SDK functions */
3
3
  deleteFilesystemByPath, deleteNetworkProcessByPidMonitor, deleteProcessByIdentifier, deleteProcessByIdentifierKill, getFilesystemByPath, getNetworkProcessByPidPorts, putCodegenFastapplyByPath, getCodegenRerankingByPath, getProcess, getProcessByIdentifier, getProcessByIdentifierLogs, getProcessByIdentifierLogsStream, postNetworkProcessByPidMonitor, postProcess, putFilesystemByPath } from "./client/index.js";
4
+ export { ResponseError, SandboxGatewayError, isGatewayError, isGatewayTimeout } from "./action.js";
4
5
  export * from "./filesystem/index.js";
5
6
  export * from "./codegen/index.js";
6
7
  export { SandboxDrive } from "./drive/index.js";