@blaxel/core 0.3.9-preview.237 → 0.3.9-preview.238
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/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/common/settings.js +2 -2
- package/dist/cjs/common/transient-retry.js +56 -6
- package/dist/cjs/sandbox/action.js +77 -17
- package/dist/cjs/sandbox/index.js +6 -1
- package/dist/cjs/types/common/transient-retry.d.ts +20 -0
- package/dist/cjs/types/sandbox/action.d.ts +20 -1
- package/dist/cjs/types/sandbox/index.d.ts +1 -0
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/common/settings.js +2 -2
- package/dist/cjs-browser/common/transient-retry.js +56 -6
- package/dist/cjs-browser/sandbox/action.js +77 -17
- package/dist/cjs-browser/sandbox/index.js +6 -1
- package/dist/cjs-browser/types/common/transient-retry.d.ts +20 -0
- package/dist/cjs-browser/types/sandbox/action.d.ts +20 -1
- package/dist/cjs-browser/types/sandbox/index.d.ts +1 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/common/settings.js +2 -2
- package/dist/esm/common/transient-retry.js +54 -6
- package/dist/esm/sandbox/action.js +73 -16
- package/dist/esm/sandbox/index.js +1 -0
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/common/settings.js +2 -2
- package/dist/esm-browser/common/transient-retry.js +54 -6
- package/dist/esm-browser/sandbox/action.js +73 -16
- package/dist/esm-browser/sandbox/index.js +1 -0
- package/package.json +1 -1
|
@@ -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.
|
|
28
|
-
const BUILD_COMMIT = "
|
|
27
|
+
const BUILD_VERSION = "0.3.9-preview.238";
|
|
28
|
+
const BUILD_COMMIT = "e8e4a67eba43ee3946e0682f613efb0e50dd0320";
|
|
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
|
|
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
|
|
60
|
-
if (node.response && typeof node.response === "object"
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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";
|