@stacksjs/browser 0.70.189 → 0.70.191
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.
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describe a non-OK HTTP response.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* const res = await fetch('/api/login', { method: 'POST', body })
|
|
7
|
+
* if (!res.ok) {
|
|
8
|
+
* const failure = describeResponseError(res.status, await res.json())
|
|
9
|
+
* error.set(failure.message)
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function describeResponseError(status: number, body?: ApiErrorBody | null): UserFacingError;
|
|
14
|
+
/**
|
|
15
|
+
* Describe something thrown while making a request.
|
|
16
|
+
*
|
|
17
|
+
* A runtime defect never reaches the returned `message`. That is the whole
|
|
18
|
+
* point: `auth is not defined` is a bug report, and it belongs in the console,
|
|
19
|
+
* not in front of the person trying to sign in.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* catch (err) {
|
|
24
|
+
* const failure = describeThrownError(err)
|
|
25
|
+
* if (failure.unexpected)
|
|
26
|
+
* console.error('[login]', failure.cause)
|
|
27
|
+
* error.set(failure.message)
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeThrownError(error: unknown): UserFacingError;
|
|
32
|
+
/**
|
|
33
|
+
* Turn a failed request, or a thrown exception, into something worth showing
|
|
34
|
+
* a person.
|
|
35
|
+
*
|
|
36
|
+
* The pattern this replaces is `catch (err) { error.set(err.message) }`. It
|
|
37
|
+
* looks like error handling and is actually a passthrough: whatever the
|
|
38
|
+
* runtime happened to say lands in the UI. A sign-in form built that way
|
|
39
|
+
* greeted people with `auth is not defined` - a programmer's ReferenceError,
|
|
40
|
+
* rendered as if it were advice - and told them nothing about what to do next.
|
|
41
|
+
*
|
|
42
|
+
* Two rules follow from that:
|
|
43
|
+
*
|
|
44
|
+
* 1. A message written by our own API for a person (`Incorrect email or
|
|
45
|
+
* password`) is the best thing to show, so prefer it.
|
|
46
|
+
* 2. A message produced by the JavaScript runtime is never shown. It names
|
|
47
|
+
* an identifier or a property in our source; to a visitor it is noise, and
|
|
48
|
+
* it leaks internals. Those become a generic sentence, and the original is
|
|
49
|
+
* kept on the result so the caller can log it.
|
|
50
|
+
*/
|
|
51
|
+
/** The error shapes a Stacks API returns. */
|
|
52
|
+
export declare interface ApiErrorBody {
|
|
53
|
+
message?: string
|
|
54
|
+
error?: string
|
|
55
|
+
errors?: Record<string, string[] | string>
|
|
56
|
+
success?: boolean
|
|
57
|
+
}
|
|
58
|
+
export declare interface UserFacingError {
|
|
59
|
+
message: string
|
|
60
|
+
fields?: Record<string, string>
|
|
61
|
+
unexpected: boolean
|
|
62
|
+
cause?: unknown
|
|
63
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
function isRuntimeDefect(error) {
|
|
2
|
+
return error instanceof ReferenceError || error instanceof SyntaxError || error instanceof RangeError || error instanceof TypeError && !isNetworkFailure(error);
|
|
3
|
+
}
|
|
4
|
+
function isNetworkFailure(error) {
|
|
5
|
+
if (!(error instanceof TypeError))
|
|
6
|
+
return !1;
|
|
7
|
+
const message = error.message.toLowerCase();
|
|
8
|
+
return message.includes("fetch") || message.includes("network") || message.includes("load failed") || message.includes("connection");
|
|
9
|
+
}
|
|
10
|
+
function flattenFieldErrors(errors) {
|
|
11
|
+
if (!errors || typeof errors !== "object")
|
|
12
|
+
return;
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const [field, value] of Object.entries(errors)) {
|
|
15
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
16
|
+
if (typeof first === "string" && first.trim())
|
|
17
|
+
out[field] = first.trim();
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
20
|
+
}
|
|
21
|
+
const UNHELPFUL_LABELS = new Set([
|
|
22
|
+
"validation failed",
|
|
23
|
+
"forbidden",
|
|
24
|
+
"unauthorized",
|
|
25
|
+
"unauthenticated",
|
|
26
|
+
"bad request",
|
|
27
|
+
"not found",
|
|
28
|
+
"internal server error",
|
|
29
|
+
"error",
|
|
30
|
+
"csrf token mismatch",
|
|
31
|
+
"invalid csrf token",
|
|
32
|
+
"token mismatch"
|
|
33
|
+
]);
|
|
34
|
+
function usableMessage(body) {
|
|
35
|
+
for (const candidate of [body?.message, body?.error]) {
|
|
36
|
+
if (typeof candidate !== "string")
|
|
37
|
+
continue;
|
|
38
|
+
const trimmed = candidate.trim();
|
|
39
|
+
if (trimmed && !UNHELPFUL_LABELS.has(trimmed.toLowerCase()))
|
|
40
|
+
return trimmed;
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
function messageForStatus(status, fields) {
|
|
45
|
+
if (status === 401)
|
|
46
|
+
return { message: "Those details did not match an account. Check them and try again.", unexpected: !1 };
|
|
47
|
+
if (status === 403)
|
|
48
|
+
return { message: "Your session expired. Refresh the page and try again.", unexpected: !1 };
|
|
49
|
+
if (status === 404)
|
|
50
|
+
return { message: "That is not available anymore.", unexpected: !1 };
|
|
51
|
+
if (status === 409)
|
|
52
|
+
return { message: "That conflicts with something that already exists.", unexpected: !1 };
|
|
53
|
+
if (status === 422)
|
|
54
|
+
return { message: (fields && Object.keys(fields).length === 1 ? Object.values(fields)[0] : void 0) ?? "Some details need fixing before this can be saved.", unexpected: !1 };
|
|
55
|
+
if (status === 429)
|
|
56
|
+
return { message: "Too many attempts. Wait a moment, then try again.", unexpected: !1 };
|
|
57
|
+
if (status >= 500)
|
|
58
|
+
return { message: "Something went wrong on our end. Try again in a moment.", unexpected: !0 };
|
|
59
|
+
return { message: "That request could not be completed. Try again.", unexpected: status >= 500 };
|
|
60
|
+
}
|
|
61
|
+
export function describeResponseError(status, body) {
|
|
62
|
+
const fields = flattenFieldErrors(body?.errors), fromBody = usableMessage(body ?? void 0), fallback = messageForStatus(status, fields);
|
|
63
|
+
return {
|
|
64
|
+
message: fromBody ?? fallback.message,
|
|
65
|
+
...fields && { fields },
|
|
66
|
+
unexpected: fallback.unexpected && !fromBody,
|
|
67
|
+
cause: body ?? void 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function describeThrownError(error) {
|
|
71
|
+
if (isNetworkFailure(error))
|
|
72
|
+
return {
|
|
73
|
+
message: "Could not reach the server. Check your connection and try again.",
|
|
74
|
+
unexpected: !1,
|
|
75
|
+
cause: error
|
|
76
|
+
};
|
|
77
|
+
if (isRuntimeDefect(error))
|
|
78
|
+
return {
|
|
79
|
+
message: "Something went wrong on our end. Try again in a moment.",
|
|
80
|
+
unexpected: !0,
|
|
81
|
+
cause: error
|
|
82
|
+
};
|
|
83
|
+
if (error instanceof Error && error.message.trim())
|
|
84
|
+
return { message: error.message.trim(), unexpected: !1, cause: error };
|
|
85
|
+
return {
|
|
86
|
+
message: "Something went wrong. Try again in a moment.",
|
|
87
|
+
unexpected: !0,
|
|
88
|
+
cause: error
|
|
89
|
+
};
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/browser",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.191",
|
|
6
6
|
"description": "Stacks core frontend/browser functionalities.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"prepublishOnly": "bun run build"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@stacksjs/composables": "0.70.
|
|
55
|
+
"@stacksjs/composables": "0.70.191",
|
|
56
56
|
"@stacksjs/stx": "^0.2.100",
|
|
57
57
|
"bun-query-builder": "^0.1.59",
|
|
58
58
|
"magic-regexp": "^0.11.0"
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"better-dx": "^0.2.17",
|
|
70
|
-
"@stacksjs/utils": "0.70.
|
|
70
|
+
"@stacksjs/utils": "0.70.191",
|
|
71
71
|
"@stripe/stripe-js": "^9.10.0"
|
|
72
72
|
}
|
|
73
73
|
}
|