Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add explicit error types for actors #1606

Closed
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
41 changes: 24 additions & 17 deletions sdks/actors/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import type { CreateRequest } from "../../manager-protocol/src/query.ts";
import { ActorHandleRaw } from "./handle.ts";
import { logger } from "./log.ts";
import * as errors from "./errors.ts";

export interface GetOpts {
parameters?: unknown;
Expand Down Expand Up @@ -189,22 +190,25 @@ export class Client {
path: string,
body?: Request,
): Promise<Response> {
const managerEndpoint = await this.#managerEndpointPromise;
const res = await fetch(`${managerEndpoint}${path}`, {
method,
headers: {
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
try {
const managerEndpoint = await this.#managerEndpointPromise;
const res = await fetch(`${managerEndpoint}${path}`, {
method,
headers: {
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});

if (!res.ok) {
throw new Error(
`Manager error (${res.statusText}):\n${await res.text()}`,
);
}
if (!res.ok) {
throw new errors.ManagerError(`${res.statusText}: ${await res.text()}`);
}

return res.json();
} catch (error) {
throw new errors.ManagerError(String(error), { cause: error });

return res.json();
}
}

async #fetchRegion(): Promise<Region | undefined> {
Expand All @@ -226,9 +230,12 @@ export class Client {
const res = await fetch(url.toString());

if (!res.ok) {
throw new Error(
`Failed to fetch region (${res.statusText}):\n${await res.text()}`,
);
// Add safe fallback in case we can't fetch the region
logger().error("failed to fetch region, defaulting to manager region", {
status: res.statusText,
body: await res.text(),
});
return undefined;
}

const { region }: { region: Region } = await res.json();
Expand Down
32 changes: 32 additions & 0 deletions sdks/actors/client/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MAX_CONN_PARAMS_SIZE } from "../../common/src/network.ts";

export class ActorClientError extends Error {

}

export class InternalError extends ActorClientError {
}

export class ManagerError extends ActorClientError {
constructor(error: string, opts?: ErrorOptions) {
super(`Manager error: ${error}`, opts);
}
}

export class ConnectionParametersTooLong extends ActorClientError {
constructor() {
super(`Connection parameters must be less than ${MAX_CONN_PARAMS_SIZE} bytes`);
}
}

export class MalformedResponseMessage extends ActorClientError {
constructor(cause?: unknown) {
super(`Malformed response message: ${cause}`, { cause });
}
}

export class RpcError extends ActorClientError {
constructor(public readonly code: string, message: string, public readonly metadata?: unknown) {
super(message);
}
}
14 changes: 7 additions & 7 deletions sdks/actors/client/src/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { assertUnreachable } from "../../common/src/utils.ts";
import * as wsToClient from "../../protocol/src/ws/to_client.ts";
import type * as wsToServer from "../../protocol/src/ws/to_server.ts";
import { logger } from "./log.ts";
import * as errors from "./errors.ts";

interface RpcInFlight {
resolve: (response: wsToClient.RpcResponseOk) => void;
Expand Down Expand Up @@ -101,9 +102,7 @@ export class ActorHandleRaw {

// TODO: This is an imprecise count since it doesn't count the full URL length & URI encoding expansion in the URL size
if (paramsStr.length > MAX_CONN_PARAMS_SIZE) {
throw new Error(
`Connection parameters must be less than ${MAX_CONN_PARAMS_SIZE} bytes`,
);
throw new errors.ConnectionParametersTooLong();
}

url += `&params=${encodeURIComponent(paramsStr)}`;
Expand Down Expand Up @@ -154,7 +153,8 @@ export class ActorHandleRaw {
ws.onmessage = (ev) => {
const rawData = ev.data;
if (typeof rawData !== "string") {
throw new Error("Response data was not string");
logger().warn("response message was not string");
throw new errors.MalformedResponseMessage();
}

const response: wsToClient.ToClient = JSON.parse(rawData);
Expand All @@ -167,14 +167,14 @@ export class ActorHandleRaw {
response.body.rpcResponseError.id,
);
inFlight.reject(
new Error(`RPC error: ${response.body.rpcResponseError.message}`),
new errors.RpcError(response.body.rpcResponseError.code, response.body.rpcResponseError.message, response.body.rpcResponseError.metadata)
);
} else if ("event" in response.body) {
this.#dispatchEvent(response.body.event);
} else if ("error" in response.body) {
logger().warn(
"unhandled error from actor",
{ message: response.body.error.message },
{ code: response.body.error.code, message: response.body.error.message, metadata: response.body.error.metadata }
);
} else {
assertUnreachable(response.body);
Expand All @@ -184,7 +184,7 @@ export class ActorHandleRaw {

#takeRpcInFlight(id: string): RpcInFlight {
const inFlight = this.#websocketRpcInFlight.get(id);
if (!inFlight) throw new Error(`No in flight response for ${id}`);
if (!inFlight) throw new errors.InternalError(`No in flight response for ${id}`);
this.#websocketRpcInFlight.delete(id);
return inFlight;
}
Expand Down
3 changes: 2 additions & 1 deletion sdks/actors/client/src/test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Client } from "./mod.ts";
import { setupLogging } from "../../common/src/log.ts";
import { InternalError } from "./errors.ts";

/**
* Uses the Rivet CLI to read the manager endpoint to connect to. This allows
Expand All @@ -12,7 +13,7 @@ export async function readEndpointFromCli(): Promise<string> {
args: ["manager", "endpoint"],
}).output();
if (!output.success) {
throw new Error(
throw new InternalError(
`Read endpoint failed with ${output.code}:\n${new TextDecoder().decode(
output.stderr,
)}`,
Expand Down
1 change: 1 addition & 0 deletions sdks/actors/common/src/network.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const PORT_NAME: string = "http";

/** Only enforced client-side in order to prevent building malformed URLs. */
export const MAX_CONN_PARAMS_SIZE = 4096;
4 changes: 4 additions & 0 deletions sdks/actors/protocol/src/ws/to_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export interface RpcResponseOk {

export interface RpcResponseError {
id: string;
code: string;
message: string;
metadata?: unknown;
}

export interface ToClientEvent {
Expand All @@ -22,5 +24,7 @@ export interface ToClientEvent {
}

export interface ToClientError {
code: string;
message: string;
metadata?: unknown;
}
Loading