feat(rpc): add service errors and retryability classification

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 13:37:52 +05:30
co-authored by Claude Opus 5
parent 3e1d7db537
commit 796b19d923
3 changed files with 126 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test";
import {
RPC_ERROR_CODES,
ServiceError,
failure,
isRetryableStatus,
success,
} from "../src/errors.ts";
describe("rpc errors", () => {
test("success and failure build the result shape", () => {
expect(success(42)).toEqual({ ok: true, value: 42 });
const f = failure(RPC_ERROR_CODES.denied, "Forbidden");
expect(f.ok).toBe(false);
if (!f.ok) {
expect(f.code).toBe("RPC_DENIED");
expect(f.retryable).toBe(false);
}
});
test("only transport failures are retryable", () => {
// 5xx and 429 are the callee saying "try again"; everything else is final.
expect(isRetryableStatus(500)).toBe(true);
expect(isRetryableStatus(503)).toBe(true);
expect(isRetryableStatus(429)).toBe(true);
expect(isRetryableStatus(400)).toBe(false);
expect(isRetryableStatus(403)).toBe(false);
expect(isRetryableStatus(404)).toBe(false);
expect(isRetryableStatus(200)).toBe(false);
});
test("a denial is never retryable", () => {
const f = failure(RPC_ERROR_CODES.denied, "Forbidden");
if (!f.ok) expect(f.retryable).toBe(false);
});
test("ServiceError carries a code and does not leak a cause into its message", () => {
const error = new ServiceError(RPC_ERROR_CODES.handler, "Something failed");
expect(error.name).toBe("ServiceError");
expect(error.code).toBe("RPC_HANDLER");
expect(error.message).toBe("Something failed");
expect(error.retryable).toBe(false);
});
test("toResult produces an opaque failure", () => {
const error = new ServiceError(RPC_ERROR_CODES.handler, "db password is hunter2");
const result = error.toResult({ exposeMessage: false });
if (!result.ok) {
expect(result.message).toBe("Internal error");
expect(result.message).not.toContain("hunter2");
}
});
});