@everystack/api 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -1
- package/package.json +1 -1
- package/src/client/index.ts +13 -3
- package/src/handler/index.ts +808 -156
- package/src/handler/service-unavailable.ts +48 -0
- package/src/handler/thrown.ts +32 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { safeRead } from './thrown';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Classify an error as an infrastructure outage: the database (or another
|
|
5
|
+
* backing service) could not be reached or dropped the connection. Duck-typed
|
|
6
|
+
* on `err.code` so the api package stays free of any driver dependency.
|
|
7
|
+
*
|
|
8
|
+
* Besides transport errors this covers SQLSTATE class 28 (a bad login): end
|
|
9
|
+
* users never log in to PostgreSQL (the handler only SET LOCAL ROLEs), so a
|
|
10
|
+
* class 28 error is the service's own credential failing, e.g. after a
|
|
11
|
+
* rotation. It also covers jose's JWKS fetch timeout (the key server is down,
|
|
12
|
+
* the token may be fine).
|
|
13
|
+
*
|
|
14
|
+
* An outage must answer 503, never 401 (a client reads 401 as "bad
|
|
15
|
+
* credentials" and logs the user out) and never echo the driver's message
|
|
16
|
+
* (it carries host and port).
|
|
17
|
+
*/
|
|
18
|
+
const UNAVAILABLE_CODES = new Set([
|
|
19
|
+
// postgres.js transport errors. CONNECTION_ENDED/CONNECTION_DESTROYED are
|
|
20
|
+
// also raised after the APP calls sql.end(), so a pool-ending bug shows as
|
|
21
|
+
// 503 rather than 500 (accepted).
|
|
22
|
+
'CONNECTION_CLOSED', 'CONNECTION_ENDED', 'CONNECTION_DESTROYED', 'CONNECT_TIMEOUT',
|
|
23
|
+
// Node socket errors
|
|
24
|
+
'ECONNREFUSED', 'ECONNRESET', 'ECONNABORTED', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN',
|
|
25
|
+
'EHOSTUNREACH', 'EHOSTDOWN', 'ENETUNREACH', 'ENETDOWN',
|
|
26
|
+
// jose: the remote JWKS fetch timed out. A third-party code, duck-typed like
|
|
27
|
+
// postgres.js. The bad-token codes (ERR_JWKS_NO_MATCHING_KEY, ERR_JWT_*)
|
|
28
|
+
// are NOT here: those stay 401.
|
|
29
|
+
'ERR_JWKS_TIMEOUT',
|
|
30
|
+
// PostgreSQL: admin_shutdown, crash_shutdown, cannot_connect_now,
|
|
31
|
+
// idle_session_timeout, too_many_connections
|
|
32
|
+
'57P01', '57P02', '57P03', '57P05', '53300',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function isServiceUnavailable(err: unknown): boolean {
|
|
36
|
+
// The error's own code wins; `cause` (one level, as fetch wraps a socket
|
|
37
|
+
// error) is read only when the error declares no code at all.
|
|
38
|
+
// A code getter that throws is a declared but unusable code: no outage, no cause.
|
|
39
|
+
const code = safeRead(err, 'code') ?? safeRead(safeRead(err, 'cause'), 'code');
|
|
40
|
+
if (typeof code !== 'string') return false;
|
|
41
|
+
// SQLSTATE class 08: connection_exception.
|
|
42
|
+
if (code.length === 5 && code.startsWith('08')) return true;
|
|
43
|
+
// SQLSTATE class 28: invalid_authorization_specification (28000, 28P01).
|
|
44
|
+
if (code.length === 5 && code.startsWith('28')) return true;
|
|
45
|
+
return UNAVAILABLE_CODES.has(code);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const SERVICE_UNAVAILABLE_MESSAGE = 'Service temporarily unavailable';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads of a thrown value. Anything can be thrown, and an Error can carry a
|
|
3
|
+
* getter that throws, so every read the handler makes of one goes through
|
|
4
|
+
* safeRead: a classifier or the fault reporter must never throw in place of
|
|
5
|
+
* the error it was handed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** What safeRead returns when the property access itself threw. */
|
|
9
|
+
export const UNREADABLE: unique symbol = Symbol('unreadable');
|
|
10
|
+
|
|
11
|
+
/** `value[key]`, or UNREADABLE when the access throws. Never throws. */
|
|
12
|
+
export function safeRead(value: unknown, key: string): unknown {
|
|
13
|
+
if (value === null || value === undefined) return undefined;
|
|
14
|
+
try {
|
|
15
|
+
return (value as Record<string, unknown>)[key];
|
|
16
|
+
} catch {
|
|
17
|
+
return UNREADABLE;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Detect PostgreSQL permission denied errors (code 42501).
|
|
23
|
+
* Used to convert Postgres GRANT/RLS denials into clean 403 responses.
|
|
24
|
+
*/
|
|
25
|
+
export function isPermissionDenied(error: unknown): boolean {
|
|
26
|
+
if (error && typeof error === 'object') {
|
|
27
|
+
if (safeRead(error, 'code') === '42501') return true;
|
|
28
|
+
const message = safeRead(error, 'message');
|
|
29
|
+
if (typeof message === 'string' && message.includes('permission denied')) return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|