1import type { APIError } from "better-auth";
2import * as Data from "effect/Data";
3import * as HttpServerResponse from "effect/http/HttpServerResponse";
4
5/**
6 * better-call stows the response headers accumulated by the endpoint
7 * (including `set-cookie` written before the failure) on the thrown
8 * `APIError` under this well-known symbol. It is registered with
9 * `Symbol.for`, so we can reference it without depending on `better-call`.
10 */
11const kAPIErrorHeaders = Symbol.for("better-call:api-error-headers");
12
13/**
14 * Structural `APIError` detection. Deliberately not `instanceof` — the
15 * error may originate from a different module instance (bundled better-auth
16 * vs peer) or realm.
17 */
18export const isAPIErrorLike = (error: unknown): error is APIError =>
19 typeof error === "object" &&
20 error !== null &&
21 error instanceof Error &&
22 error.name === "APIError" &&
23 typeof (error as APIError).statusCode === "number";
24
25/**
26 * Merge an `APIError`'s visible `headers` with the hidden response headers
27 * better-call carries on {@link kAPIErrorHeaders}. `set-cookie` values are
28 * appended (never collapsed) so session cookies written before the failure
29 * survive.
30 */
31export const mergeAPIErrorHeaders = (error: APIError): Headers => {
32 const merged = new Headers(error.headers as HeadersInit | undefined);
33 const hidden = (error as unknown as Record<symbol, unknown>)[
34 kAPIErrorHeaders
35 ];
36 if (hidden instanceof Headers) {
37 const cookies =
38 typeof hidden.getSetCookie === "function" ? hidden.getSetCookie() : [];
39 hidden.forEach((value, key) => {
40 if (key.toLowerCase() !== "set-cookie") {
41 merged.set(key, value);
42 }
43 });
44 for (const cookie of cookies) {
45 merged.append("set-cookie", cookie);
46 }
47 }
48 return merged;
49};
50
51/**
52 * Typed failure of an effectified `auth.api.*` call.
53 *
54 * Better Auth throws `APIError` from `better-call`; this error carries the
55 * same information as first-class fields:
56 *
57 * - `status` — the better-call status KEY, e.g. `"UNAUTHORIZED"` (a string)
58 * - `statusCode` — the numeric HTTP status, e.g. `401`
59 * - `body` — the JSON error body (`message`, `code`, ...)
60 * - `headers` — merged response headers, INCLUDING the `set-cookie`
61 * headers better-call hides on a symbol property of the thrown error
62 *
63 * Match on `statusCode` (or `body.code`) — `status` is a string key, not a
64 * number.
65 */
66export class BetterAuthApiError extends Data.TaggedError("BetterAuthApiError")<{
67 readonly status: string;
68 readonly statusCode: number;
69 readonly body:
70 | ({ message?: string; code?: string } & Record<string, unknown>)
71 | undefined;
72 readonly headers: Headers;
73 readonly cause: APIError;
74}> {
75 static fromAPIError(error: APIError): BetterAuthApiError {
76 return new BetterAuthApiError({
77 status: String(error.status),
78 statusCode: error.statusCode,
79 body: error.body as BetterAuthApiError["body"],
80 headers: mergeAPIErrorHeaders(error),
81 cause: error,
82 });
83 }
84
85 override get message(): string {
86 return this.body?.message ?? `Better Auth API error (${this.statusCode})`;
87 }
88
89 /**
90 * Render the error as an HTTP response (status + JSON body + merged
91 * headers) — for handlers that want to pass the failure through to the
92 * client exactly as Better Auth would have.
93 */
94 toResponse(): HttpServerResponse.HttpServerResponse {
95 return HttpServerResponse.fromWeb(
96 new Response(JSON.stringify(this.body ?? {}), {
97 status: this.statusCode,
98 headers: withJsonContentType(this.headers),
99 }),
100 );
101 }
102}
103
104const withJsonContentType = (headers: Headers): Headers => {
105 const out = new Headers(headers);
106 if (!out.has("content-type")) {
107 out.set("content-type", "application/json");
108 }
109 return out;
110};
111
112/** Failure of the deploy-time schema migration. */
113export class BetterAuthMigrationError extends Data.TaggedError(
114 "BetterAuthMigrationError",
115)<{
116 readonly message: string;
117 readonly cause?: unknown;
118}> {}
119
120/** Failure of a `secondaryStorage` operation. */
121export class BetterAuthStorageError extends Data.TaggedError(
122 "BetterAuthStorageError",
123)<{
124 readonly message: string;
125 readonly cause?: unknown;
126}> {}