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
2 changes: 1 addition & 1 deletion docs-internal/engine/napi-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Rules for `rivetkit-typescript/packages/rivetkit-napi/`. The bridge is pure plum
## Payload + error conventions

- `#[napi(object)]` bridge payloads stay plain-data only. If TypeScript needs to cancel native work, use primitives or JS-side polling instead of trying to pass a `#[napi]` class instance through an object field.
- N-API structured errors cross the JS<->Rust boundary by prefix-encoding `{ group, code, message, metadata }` into `napi::Error.reason`, then normalizing that prefix back into a `RivetError` on the other side.
- N-API structured errors cross the JS<->Rust boundary by prefix-encoding `{ group, code, message, metadata, rayId }` into `napi::Error.reason`, then normalizing that prefix back into a `RivetError` on the other side.
- N-API bridge debug logs use stable `kind` plus compact payload summaries, never raw buffers or full request bodies.

## Receive-loop state lifecycle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ impl ActorContext {
message: Some(message),
public_: Some(true),
status_code: Some(401),
ray_id: None,
}))
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ struct BridgeRivetErrorPayload {
code: String,
message: String,
metadata: Option<serde_json::Value>,
#[serde(rename = "rayId")]
ray_id: Option<String>,
#[serde(rename = "public")]
public_: Option<bool>,
#[serde(rename = "statusCode")]
Expand All @@ -281,6 +283,7 @@ pub(crate) struct BridgeRivetErrorContext {
pub message: Option<String>,
pub public_: Option<bool>,
pub status_code: Option<u16>,
pub ray_id: Option<String>,
}

impl std::fmt::Display for BridgeRivetErrorContext {
Expand Down Expand Up @@ -1010,6 +1013,7 @@ fn parse_bridge_rivet_error(reason: &str) -> Option<anyhow::Error> {
message: Some(message),
public_: payload.public_,
status_code: payload.status_code,
ray_id: payload.ray_id,
}))
}

Expand Down
1 change: 1 addition & 0 deletions rivetkit-typescript/packages/rivetkit-napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ fn anyhow_to_bridge_rivet_error_payload(error: anyhow::Error) -> serde_json::Val
"code": error.code(),
"message": error.message(),
"metadata": error.metadata(),
"rayId": bridge_context.and_then(|context| context.ray_id.as_deref()),
"public": public_,
"statusCode": status_code,
"actor": error.actor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ mod moved_tests {
"code": "same_code",
"message": "same message",
"metadata": { "count": 1 },
"rayId": "ray-123",
})
);

Expand All @@ -76,6 +77,12 @@ mod moved_tests {
assert!(transport_error(&first).schema().is_none());
assert_eq!(transport_error(&second).group(), "actor");
assert_eq!(transport_error(&second).code(), "same_code");

let payload = crate::anyhow_to_bridge_rivet_error_payload(first);
assert_eq!(
payload.get("rayId").and_then(|value| value.as_str()),
Some("ray-123")
);
}

#[test]
Expand Down
14 changes: 14 additions & 0 deletions rivetkit-typescript/packages/rivetkit/src/actor/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface RivetErrorOptions extends ErrorOptions {
public?: boolean;
/** Metadata associated with this error. */
metadata?: unknown;
/** Request identifier used to correlate this error with engine logs. */
rayId?: string;
/** Explicit HTTP status override for router responses. */
statusCode?: number;
/** Actor context associated with this error. */
Expand All @@ -31,6 +33,7 @@ export interface RivetErrorLike {
code: string;
message: string;
metadata?: unknown;
rayId?: string;
public?: boolean;
statusCode?: number;
actor?: ActorSpecifier;
Expand Down Expand Up @@ -59,6 +62,7 @@ function looksLikeRivetErrorOptions(
value !== null &&
("public" in value ||
"metadata" in value ||
"rayId" in value ||
"statusCode" in value ||
"actor" in value ||
"cause" in value)
Expand Down Expand Up @@ -94,6 +98,9 @@ export function isRivetErrorLike(
typeof error.code === "string" &&
"message" in error &&
typeof error.message === "string" &&
(!("rayId" in error) ||
error.rayId === undefined ||
typeof error.rayId === "string") &&
(!("__type" in error) || isTypedErrorTag(error.__type))
);
}
Expand Down Expand Up @@ -128,6 +135,7 @@ export class RivetError extends Error {

public public: boolean;
public metadata?: unknown;
public readonly rayId?: string;
public statusCode: number;
public actor?: ActorSpecifier;
public readonly group: string;
Expand Down Expand Up @@ -161,6 +169,7 @@ export class RivetError extends Error {
this.code = code;
this.public = normalized.public ?? false;
this.metadata = normalized.metadata;
this.rayId = normalized.rayId;
this.statusCode = normalized.statusCode ?? (this.public ? 400 : 500);
this.actor = normalized.actor;
}
Expand Down Expand Up @@ -205,6 +214,7 @@ export function toRivetError(
public: error.public,
statusCode: error.statusCode,
metadata: error.metadata,
rayId: error.rayId,
actor: error.actor,
cause: error instanceof Error ? error.cause : undefined,
});
Expand All @@ -218,6 +228,7 @@ export function toRivetError(
public: fallback?.public,
statusCode: fallback?.statusCode,
metadata: fallback?.metadata,
rayId: fallback?.rayId,
actor: fallback?.actor,
cause: error instanceof Error ? error : undefined,
},
Expand All @@ -230,6 +241,7 @@ export function encodeBridgeRivetError(error: RivetErrorLike): string {
code: error.code,
message: error.message,
metadata: error.metadata,
rayId: error.rayId,
public: error.public,
statusCode: error.statusCode,
actor: error.actor,
Expand Down Expand Up @@ -272,6 +284,7 @@ export function decodeBridgeRivetError(value: string): RivetError | undefined {

return new RivetError(payload.group, payload.code, payload.message, {
metadata: payload.metadata,
rayId: payload.rayId,
public: payload.public,
statusCode: payload.statusCode,
actor: payload.actor ?? undefined,
Expand Down Expand Up @@ -303,6 +316,7 @@ export function internalError(
public: options?.public,
statusCode: options?.statusCode,
metadata: options?.metadata,
rayId: options?.rayId,
actor: options?.actor,
cause: options?.cause,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ export class ActorConnRaw {
const parsed = parseWebSocketCloseReason(reason);

if (parsed) {
const { group, code } = parsed;
const { group, code, rayId } = parsed;

if (this.#shouldReconnectForStaleActor(group, code)) {
this.#clearResolvedActorIdentity();
Expand All @@ -883,7 +883,7 @@ export class ActorConnRaw {
group,
code,
`Connection closed: ${reason}`,
undefined,
{ rayId },
),
);
return;
Expand All @@ -897,6 +897,7 @@ export class ActorConnRaw {
this.#actorId,
this.#actorResolutionState,
this.#driver,
rayId,
);
if (schedulingError) {
error = schedulingError;
Expand All @@ -905,15 +906,15 @@ export class ActorConnRaw {
group,
code,
`Connection closed: ${reason}`,
undefined,
{ rayId },
);
}
} else {
error = new errors.ActorError(
group,
code,
`Connection closed: ${reason}`,
undefined,
{ rayId },
);
}

Expand Down
41 changes: 32 additions & 9 deletions rivetkit-typescript/packages/rivetkit/src/client/actor-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class ActorHandleRaw {
},
}).send(name, body, options as any);
} catch (err) {
const { group, code, message, metadata, actor } =
const { group, code, message, metadata, rayId, actor } =
deconstructError(err, true);

if (
Expand All @@ -189,6 +189,7 @@ export class ActorHandleRaw {
actorId,
attempt,
maxAttempts,
rayId,
)
) {
useQueryTarget = true;
Expand Down Expand Up @@ -227,6 +228,7 @@ export class ActorHandleRaw {

throw new ActorError(group, code, message, {
metadata,
rayId,
actor,
});
}
Expand Down Expand Up @@ -357,7 +359,7 @@ export class ActorHandleRaw {
}
return output;
} catch (err) {
const { group, code, message, metadata, actor } =
const { group, code, message, metadata, rayId, actor } =
deconstructError(err, true);

if (
Expand All @@ -367,6 +369,7 @@ export class ActorHandleRaw {
actorId,
attempt,
maxAttempts,
rayId,
)
) {
useQueryTarget = true;
Expand Down Expand Up @@ -398,7 +401,7 @@ export class ActorHandleRaw {
"actor",
"not_found",
"The actor does not exist or was destroyed.",
{ metadata, actor },
{ metadata, rayId, actor },
);
}

Expand All @@ -419,7 +422,11 @@ export class ActorHandleRaw {
continue;
}

throw new ActorError(group, code, message, { metadata, actor });
throw new ActorError(group, code, message, {
metadata,
rayId,
actor,
});
}
}

Expand Down Expand Up @@ -514,6 +521,7 @@ export class ActorHandleRaw {
actorId: string | undefined,
attempt: number,
maxAttempts: number,
rayId?: string,
): Promise<boolean> {
if (
!isDynamicActorQuery(this.#actorResolutionState) ||
Expand All @@ -530,6 +538,7 @@ export class ActorHandleRaw {
actorId,
this.#actorResolutionState,
this.#driver,
rayId,
);
if (schedulingError) {
throw schedulingError;
Expand Down Expand Up @@ -679,7 +688,7 @@ export class ActorHandleRaw {
}
return response;
} catch (err) {
const { group, code, message, metadata, actor } =
const { group, code, message, metadata, rayId, actor } =
deconstructError(err, true);

if (
Expand All @@ -689,6 +698,7 @@ export class ActorHandleRaw {
actorId,
attempt,
maxAttempts,
rayId,
)
) {
useQueryTarget = true;
Expand Down Expand Up @@ -725,7 +735,11 @@ export class ActorHandleRaw {
continue;
}

throw new ActorError(group, code, message, { metadata, actor });
throw new ActorError(group, code, message, {
metadata,
rayId,
actor,
});
}
}

Expand All @@ -750,7 +764,7 @@ export class ActorHandleRaw {
return null;
}

const { group, code } = error;
const { group, code, rayId } = error;

if (
await this.#shouldRetrySchedulingError(
Expand All @@ -759,6 +773,7 @@ export class ActorHandleRaw {
actorId,
attempt,
maxAttempts,
rayId,
)
) {
return {
Expand Down Expand Up @@ -802,6 +817,7 @@ export class ActorHandleRaw {
code: string;
message: string;
metadata?: unknown;
rayId?: string;
actor?: ActorSpecifier;
} | null> {
if (response.ok) {
Expand All @@ -814,14 +830,15 @@ export class ActorHandleRaw {
: this.#encoding;

try {
return deserializeWithEncoding<
const error = deserializeWithEncoding<
protocol.HttpResponseError,
HttpResponseErrorJson,
{
group: string;
code: string;
message: string;
metadata?: unknown;
rayId?: string;
actor?: ActorSpecifier;
}
>(
Expand Down Expand Up @@ -854,6 +871,10 @@ export class ActorHandleRaw {
: undefined,
}),
);
return {
...error,
rayId: response.headers.get("x-rivet-ray-id") ?? undefined,
};
} catch {
return null;
}
Expand Down Expand Up @@ -927,7 +948,9 @@ export class ActorHandleRaw {
"actor",
"reload_failed",
`reload failed with status ${response.status}: ${body}`,
{},
{
rayId: response.headers.get("x-rivet-ray-id") ?? undefined,
},
);
}
}
Expand Down
Loading
Loading