Skip to content
Open
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
28 changes: 28 additions & 0 deletions @blaxel/core/src/client/errorCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Stable error codes emitted by the Blaxel gateway proxy via the
* `X-Blaxel-Error-Code` response header and the `error.code` JSON body field.
*
* @example
* ```typescript
* import { GatewayError, ERR_WORKLOAD_UNAVAILABLE } from "@blaxel/core";
*
* try {
* await someApiCall();
* } catch (err) {
* if (err instanceof GatewayError && err.errorCode === ERR_WORKLOAD_UNAVAILABLE) {
* // retry with backoff
* }
* }
* ```
*/

export const ERR_ROUTE_NOT_FOUND = "ROUTE_NOT_FOUND" as const;
export const ERR_WORKLOAD_NOT_FOUND = "WORKLOAD_NOT_FOUND" as const;
export const ERR_WORKSPACE_NOT_FOUND = "WORKSPACE_NOT_FOUND" as const;
export const ERR_WORKLOAD_UNAVAILABLE = "WORKLOAD_UNAVAILABLE" as const;
export const ERR_AUTHENTICATION_REQUIRED = "AUTHENTICATION_REQUIRED" as const;
export const ERR_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED" as const;
export const ERR_FORBIDDEN = "FORBIDDEN" as const;
export const ERR_BAD_REQUEST = "BAD_REQUEST" as const;
export const ERR_USAGE_LIMIT_EXCEEDED = "USAGE_LIMIT_EXCEEDED" as const;
export const ERR_POLICY_VIOLATION = "POLICY_VIOLATION" as const;
45 changes: 45 additions & 0 deletions @blaxel/core/src/client/gatewayError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Error thrown when the Blaxel gateway proxy synthesizes an error response.
*
* The gateway sets the `X-Blaxel-Source: platform` header on every response it
* generates itself (as opposed to forwarding from the upstream workload). This
* class exposes the stable error code and agent-readable metadata so callers
* can branch on {@link errorCode} instead of parsing free-text messages.
*/
export class GatewayError extends Error {
/** Stable error code from the `X-Blaxel-Error-Code` header. */
readonly errorCode: string;
/** HTTP status code of the gateway response. */
readonly statusCode: number;
/** Whether retrying the same request may succeed. */
readonly retryable: boolean;
/** Directive telling the caller what to do next. */
readonly action: string;
/** Anti-pattern warning (may be undefined). */
readonly doNot?: string;
/** Link to the relevant documentation page (may be undefined). */
readonly docsUrl?: string;
/** The raw gateway response. */
readonly response: Response;

constructor(opts: {
errorCode: string;
message: string;
statusCode: number;
retryable: boolean;
action: string;
doNot?: string;
docsUrl?: string;
response: Response;
}) {
super(opts.message);
this.name = "GatewayError";
this.errorCode = opts.errorCode;
this.statusCode = opts.statusCode;
this.retryable = opts.retryable;
this.action = opts.action;
this.doNot = opts.doNot;
this.docsUrl = opts.docsUrl;
this.response = opts.response;
}
}
51 changes: 49 additions & 2 deletions @blaxel/core/src/client/responseInterceptor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,58 @@
/**
* Response interceptor that enhances authentication error messages (401/403)
* with a link to the authentication documentation.
* Response interceptors for the Blaxel SDK.
*
* - Gateway error interceptor: detects gateway-synthesized errors and throws GatewayError
* - Authentication error interceptor: enhances 401/403 messages with doc links
*/

import { GatewayError } from "./gatewayError.js";

type ResponseInterceptor = (
response: Response
) => Promise<Response>;

/**
* Intercepts HTTP responses from the Blaxel gateway proxy and throws a
* {@link GatewayError} when the response was synthesized by the gateway
* (identified by the `X-Blaxel-Source: platform` header).
*/
export const gatewayErrorInterceptor: ResponseInterceptor = async (
response: Response
) => {
if (response.headers.get("X-Blaxel-Source") !== "platform") {
return response;
}
Comment thread
mendral-app[bot] marked this conversation as resolved.

if (response.ok) {
return response;
}

const cloned = response.clone();
let errorObj: Record<string, unknown> = {};
try {
const body: unknown = await cloned.json();
if (body && typeof body === "object" && "error" in (body as Record<string, unknown>)) {
const raw = (body as Record<string, unknown>).error;
if (raw && typeof raw === "object") {
errorObj = raw as Record<string, unknown>;
}
}
} catch {
// body is not JSON — proceed with empty errorObj
}

throw new GatewayError({
errorCode: response.headers.get("X-Blaxel-Error-Code") ?? "",
message: typeof errorObj.message === "string" ? errorObj.message : response.statusText,
statusCode: response.status,
retryable: Boolean(errorObj.retryable),
action: typeof errorObj.action === "string" ? errorObj.action : "",
doNot: typeof errorObj.do_not === "string" ? errorObj.do_not : undefined,
docsUrl: typeof errorObj.docs_url === "string" ? errorObj.docs_url : undefined,
response,
});
};

/**
* Intercepts HTTP responses and adds authentication documentation
* to 401/403 error responses
Expand Down Expand Up @@ -58,6 +104,7 @@ export const authenticationErrorInterceptor: ResponseInterceptor = async (
};

export const responseInterceptors: ResponseInterceptor[] = [
gatewayErrorInterceptor,
authenticationErrorInterceptor,
];

2 changes: 2 additions & 0 deletions @blaxel/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import './common/autoload';
export * from "./agents/index.js";
export * from "./client/client.js";
export * from "./client/errorCodes.js";
export * from "./client/gatewayError.js";
export * from "./common/autoload.js";
export * from "./common/env.js";
export * from "./common/node.js";
Expand Down
22 changes: 21 additions & 1 deletion @blaxel/core/src/sandbox/action.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { createClient, type Client } from "@hey-api/client-fetch";
import { GatewayError } from "../client/gatewayError.js";
import { interceptors } from "../client/interceptors.js";
import { responseInterceptors } from "../client/responseInterceptor.js";
import { createH2Fetch, h2RequestDirect } from "../common/h2fetch.js";
import { getForcedUrl, getGlobalUniqueHash } from "../common/internal.js";
import { settings } from "../common/settings.js";
import { client as defaultClient } from "./client/client.gen.js";
import { SandboxConfiguration } from "./types.js";
import type { SandboxConfiguration } from "./types.js";

export class ResponseError extends Error {
constructor(public response: Response, public data: unknown, public error: unknown) {
Expand Down Expand Up @@ -112,6 +113,25 @@ export class SandboxAction {

handleResponseError(response: Response, data: unknown, error: unknown) {
if (!response.ok || !data) {
if (response.headers.get("X-Blaxel-Source") === "platform") {
const errorObj =
data && typeof data === "object" && "error" in data
? (data as Record<string, unknown>).error as Record<string, unknown> | undefined
: undefined;
throw new GatewayError({
errorCode: response.headers.get("X-Blaxel-Error-Code") ?? "",
message:
typeof errorObj?.message === "string"
? errorObj.message
: response.statusText,
statusCode: response.status,
retryable: Boolean(errorObj?.retryable),
action: typeof errorObj?.action === "string" ? errorObj.action : "",
doNot: typeof errorObj?.do_not === "string" ? errorObj.do_not : undefined,
docsUrl: typeof errorObj?.docs_url === "string" ? errorObj.docs_url : undefined,
response,
});
}
throw new ResponseError(response, data, error);
}
}
Expand Down
Loading