@faststore/core 4.2.0 → 4.3.0-dev.1
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/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +75 -26
- package/CHANGELOG.md +10 -0
- package/package.json +6 -6
- package/src/pages/api/graphql.ts +59 -4
- package/test/pages/api/graphql.test.ts +167 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.
|
|
2
|
+
> @faststore/core@4.3.0-dev.0 generate /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> pnpm run gen-types && pnpm run cache-graphql
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @faststore/core@4.
|
|
6
|
+
> @faststore/core@4.3.0-dev.0 gen-types /home/runner/work/faststore/faststore/packages/core
|
|
7
7
|
> node ../cli/bin/run generate-types .
|
|
8
8
|
|
|
9
9
|
[33m[STARTED][39m Parse Configuration
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
[32m[COMPLETED][39m Generate to /home/runner/work/faststore/faststore/packages/core/@generated/
|
|
20
20
|
[32m[COMPLETED][39m Generate outputs
|
|
21
21
|
|
|
22
|
-
> @faststore/core@4.
|
|
22
|
+
> @faststore/core@4.3.0-dev.0 cache-graphql /home/runner/work/faststore/faststore/packages/core
|
|
23
23
|
> node ../cli/bin/run cache-graphql --config=./discovery.config.default.js --queries=./@generated/persisted-documents.json
|
|
24
24
|
|
|
25
25
|
[Info] - Config file location: /home/runner/work/faststore/faststore/packages/core/discovery.config.default.js
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@4.
|
|
2
|
+
> @faststore/core@4.3.0-dev.0 test /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[46m RUN [49m[22m [36mv4.0.7 [39m[90m/home/runner/work/faststore/faststore/packages/core[39m
|
|
7
7
|
|
|
8
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/installLocaleCorrector.browser.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 54[2mms[22m[39m
|
|
9
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 33[2mms[22m[39m
|
|
10
8
|
[90mstderr[2m | test/sdk/localization/store-url.browser.test.ts
|
|
11
9
|
[22m[39mError in optimistic validation: TypeError: Cannot read properties of undefined (reading 'read')
|
|
12
10
|
at validateCart [90m(/home/runner/work/faststore/faststore/packages/core/[39msrc/sdk/cart/index.ts:119:27[90m)[39m
|
|
@@ -14,28 +12,79 @@
|
|
|
14
12
|
at a (/home/runner/work/faststore/faststore/packages/sdk/dist/es/index.mjs:353:23)
|
|
15
13
|
[90m at processTicksAndRejections (node:internal/process/task_queues:103:5)[39m
|
|
16
14
|
|
|
17
|
-
[32m✓[39m [30m[
|
|
18
|
-
[32m✓[39m [30m[46m browser [49m[39m test/
|
|
19
|
-
[32m✓[39m [30m[43m node [49m[39m test/utils/
|
|
20
|
-
[32m✓[39m [30m[46m browser [49m[39m test/sdk/localization/store-url.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m
|
|
21
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
22
|
-
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/
|
|
23
|
-
[32m✓[39m [30m[
|
|
15
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/getInitialSession.browser.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 42[2mms[22m[39m
|
|
16
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/session/installLocaleCorrector.browser.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 62[2mms[22m[39m
|
|
17
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/localization/bindingPaths.test.ts [2m([22m[2m71 tests[22m[2m)[22m[32m 74[2mms[22m[39m
|
|
18
|
+
[32m✓[39m [30m[46m browser [49m[39m test/sdk/localization/store-url.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
19
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/match-url.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 38[2mms[22m[39m
|
|
20
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/bindingSelector.test.ts [2m([22m[2m18 tests[22m[2m)[22m[32m 23[2mms[22m[39m
|
|
21
|
+
[32m✓[39m [30m[46m browser [49m[39m test/utils/isLocalHost.browser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
|
22
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/useBindingSelector.test.tsx [2m([22m[2m12 tests[22m[2m)[22m[32m 20[2mms[22m[39m
|
|
23
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/cookieCacheBusting.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 44[2mms[22m[39m
|
|
24
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/search/useSearchHistory.test.ts [2m([22m[2m13 tests[22m[2m)[22m[32m 224[2mms[22m[39m
|
|
24
25
|
[32m✓[39m [30m[43m node [49m[39m test/utils/multipleTemplates.test.ts [2m([22m[2m8 tests[22m[2m)[22m[32m 15[2mms[22m[39m
|
|
25
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
26
|
-
[32m✓[39m [30m[43m node [49m[39m test/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
[32m
|
|
31
|
-
[
|
|
32
|
-
[32m
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[
|
|
38
|
-
|
|
39
|
-
[2m
|
|
40
|
-
[
|
|
26
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/clearCookies.test.ts [2m([22m[2m20 tests[22m[2m)[22m[32m 147[2mms[22m[39m
|
|
27
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/content/service.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 12[2mms[22m[39m
|
|
28
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates the upstream 400 from a wrapped BadRequestError instead of 500
|
|
29
|
+
[22m[39mGraphql execution returned with error: [
|
|
30
|
+
{
|
|
31
|
+
message: [32m'O campo CEP (88) é inválido'[39m,
|
|
32
|
+
status: [33m400[39m,
|
|
33
|
+
type: [32m'BadRequestError'[39m
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates UnauthorizedError status
|
|
38
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'nope'[39m, status: [33m401[39m, type: [32m'UnauthorizedError'[39m } ]
|
|
39
|
+
|
|
40
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates ForbiddenError status
|
|
41
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'nope'[39m, status: [33m403[39m, type: [32m'ForbiddenError'[39m } ]
|
|
42
|
+
|
|
43
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates NotFoundError status
|
|
44
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'nope'[39m, status: [33m404[39m, type: [32m'NotFoundError'[39m } ]
|
|
45
|
+
|
|
46
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mpropagates an unmapped upstream status (UnknownError 429)
|
|
47
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'slow down'[39m, status: [33m429[39m, type: [32m'UnknownError'[39m } ]
|
|
48
|
+
|
|
49
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mrecovers status when the error itself is a FastStoreError (not wrapped)
|
|
50
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'bad input'[39m, status: [33m400[39m, type: [32m'BadRequestError'[39m } ]
|
|
51
|
+
|
|
52
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mfalls back to a body-less 500 for non-FastStore errors
|
|
53
|
+
[22m[39mGraphql execution returned with error: [
|
|
54
|
+
{
|
|
55
|
+
message: [32m'Sorry, something went wrong.'[39m,
|
|
56
|
+
status: [90mundefined[39m,
|
|
57
|
+
type: [90mundefined[39m
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2mexposes type, status and message in the body outside production
|
|
62
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'invalid CEP'[39m, status: [33m400[39m, type: [32m'BadRequestError'[39m } ]
|
|
63
|
+
|
|
64
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2momits the free-text message from the body in production
|
|
65
|
+
[22m[39mGraphql execution returned with error: [ { message: [32m'invalid CEP'[39m, status: [33m400[39m, type: [32m'BadRequestError'[39m } ]
|
|
66
|
+
|
|
67
|
+
[90mstderr[2m | test/pages/api/graphql.test.ts[2m > [22m[2m/api/graphql error status propagation[2m > [22m[2muses the first recoverable FastStoreError when several errors exist
|
|
68
|
+
[22m[39mGraphql execution returned with error: [
|
|
69
|
+
{ message: [32m'plain error'[39m, status: [90mundefined[39m, type: [90mundefined[39m },
|
|
70
|
+
{ message: [32m'missing'[39m, status: [33m404[39m, type: [32m'NotFoundError'[39m },
|
|
71
|
+
{ message: [32m'bad'[39m, status: [33m400[39m, type: [32m'BadRequestError'[39m }
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/graphql.test.ts [2m([22m[2m10 tests[22m[2m)[22m[32m 72[2mms[22m[39m
|
|
75
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/cms/global.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 19[2mms[22m[39m
|
|
76
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/getRequestHostname.test.ts [2m([22m[2m12 tests[22m[2m)[22m[32m 23[2mms[22m[39m
|
|
77
|
+
[32m✓[39m [30m[43m node [49m[39m test/sdk/localization/store-url.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 12[2mms[22m[39m
|
|
78
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/validateSessionRefreshToken.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 14[2mms[22m[39m
|
|
79
|
+
[32m✓[39m [30m[43m node [49m[39m test/pages/api/preview.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 18[2mms[22m[39m
|
|
80
|
+
[32m✓[39m [30m[43m node [49m[39m test/utils/isLocalHost.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 8[2mms[22m[39m
|
|
81
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/cms/index.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
82
|
+
[32m✓[39m [30m[43m node [49m[39m test/server/index.test.ts [2m([22m[2m7 tests[22m[2m)[22m[33m 1333[2mms[22m[39m
|
|
83
|
+
[33m[2m✓[22m[39m should return a valid merged GraphQL schema [33m 658[2mms[22m[39m
|
|
84
|
+
[33m[2m✓[22m[39m should exist with its plugins [33m 390[2mms[22m[39m
|
|
85
|
+
|
|
86
|
+
[2m Test Files [22m [1m[32m22 passed[39m[22m[90m (22)[39m
|
|
87
|
+
[2m Tests [22m [1m[32m236 passed[39m[22m[90m (236)[39m
|
|
88
|
+
[2m Start at [22m 14:05:33
|
|
89
|
+
[2m Duration [22m 15.91s[2m (transform 5.65s, setup 0ms, collect 17.89s, tests 2.25s, environment 14.22s, prepare 794ms)[22m
|
|
41
90
|
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [4.3.0-dev.1](https://github.com/vtex/faststore/compare/v4.3.0-dev.0...v4.3.0-dev.1) (2026-06-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **core:** propagate upstream error status instead of always 500 ([#3379](https://github.com/vtex/faststore/issues/3379)) ([c03d3df](https://github.com/vtex/faststore/commit/c03d3dff7f5920e834a318417a4a02cc653453cf))
|
|
11
|
+
|
|
12
|
+
# [4.3.0-dev.0](https://github.com/vtex/faststore/compare/v4.2.0...v4.3.0-dev.0) (2026-06-01)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @faststore/core
|
|
15
|
+
|
|
6
16
|
# [4.2.0](https://github.com/vtex/faststore/compare/v4.1.2...v4.2.0) (2026-06-01)
|
|
7
17
|
|
|
8
18
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0-dev.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"style-loader": "^3.3.1",
|
|
71
71
|
"swr": "^2.2.5",
|
|
72
72
|
"use-sync-external-store": "^1.6.0",
|
|
73
|
-
"@faststore/
|
|
74
|
-
"@faststore/
|
|
75
|
-
"@faststore/
|
|
76
|
-
"@faststore/
|
|
77
|
-
"@faststore/ui": "4.
|
|
73
|
+
"@faststore/diagnostics": "4.3.0-dev.1",
|
|
74
|
+
"@faststore/api": "4.3.0-dev.1",
|
|
75
|
+
"@faststore/sdk": "4.3.0-dev.1",
|
|
76
|
+
"@faststore/lighthouse": "4.3.0-dev.1",
|
|
77
|
+
"@faststore/ui": "4.3.0-dev.1"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@cypress/code-coverage": "^3.12.1",
|
package/src/pages/api/graphql.ts
CHANGED
|
@@ -18,6 +18,21 @@ const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes
|
|
|
18
18
|
const DEFAULT_STALE_WHILE_REVALIDATE = 60 * 60 // 1 hour
|
|
19
19
|
const ALLOWED_HOST_SUFFIXES = ['localhost', '.vtex.app', '.localhost']
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Recovers a FastStoreError from an execution error, whether it is the error
|
|
23
|
+
* itself or wrapped as the `originalError` of a masked GraphQLError.
|
|
24
|
+
*/
|
|
25
|
+
const recoverFastStoreError = (graphqlError: unknown) => {
|
|
26
|
+
if (isFastStoreError(graphqlError)) {
|
|
27
|
+
return graphqlError
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const originalError = (graphqlError as { originalError?: unknown })
|
|
31
|
+
?.originalError
|
|
32
|
+
|
|
33
|
+
return isFastStoreError(originalError) ? originalError : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
// Example: "Set-Cookie: key=value; Domain=example.com; Path=/"
|
|
22
37
|
const MATCH_DOMAIN_REGEXP = /(?:^|;\s*)(?:domain=)([^;]+)/i
|
|
23
38
|
|
|
@@ -186,13 +201,53 @@ const handler: NextApiHandler = async (request, response) => {
|
|
|
186
201
|
{ headers: request.headers }
|
|
187
202
|
)
|
|
188
203
|
|
|
189
|
-
const hasErrors = Array.isArray(errors)
|
|
204
|
+
const hasErrors = Array.isArray(errors) && errors.length > 0
|
|
190
205
|
|
|
191
206
|
if (hasErrors) {
|
|
192
|
-
|
|
193
|
-
|
|
207
|
+
// After error masking, entries are GraphQLError instances whose
|
|
208
|
+
// `name` is "GraphQLError" — the original FastStoreError (carrying the
|
|
209
|
+
// upstream status) is nested in `originalError`. Recover it so the BFF
|
|
210
|
+
// propagates the real status instead of collapsing everything to 500.
|
|
211
|
+
const fastStoreError = errors.map(recoverFastStoreError).find(Boolean)
|
|
212
|
+
|
|
213
|
+
console.error(
|
|
214
|
+
'Graphql execution returned with error: ',
|
|
215
|
+
errors.map((graphqlError) => {
|
|
216
|
+
const fsError = recoverFastStoreError(graphqlError)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
message: (graphqlError as { message?: string })?.message,
|
|
220
|
+
status: fsError?.extensions.status,
|
|
221
|
+
type: fsError?.extensions.type,
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const status = fastStoreError?.extensions.status ?? 500
|
|
227
|
+
|
|
228
|
+
// No recoverable FastStoreError: keep the masked, body-less 500.
|
|
229
|
+
if (!fastStoreError) {
|
|
230
|
+
response.status(status).end()
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Only FastStoreError-derived details are exposed. `type`/`status` are a
|
|
235
|
+
// closed enum (safe everywhere); the free-text `message` may echo raw
|
|
236
|
+
// upstream text, so it is restricted to non-production responses. The
|
|
237
|
+
// full message is still available server-side via the log above.
|
|
238
|
+
const responseError = {
|
|
239
|
+
extensions: {
|
|
240
|
+
type: fastStoreError.extensions.type,
|
|
241
|
+
status: fastStoreError.extensions.status,
|
|
242
|
+
},
|
|
243
|
+
...(process.env.NODE_ENV !== 'production'
|
|
244
|
+
? { message: fastStoreError.message }
|
|
245
|
+
: {}),
|
|
246
|
+
}
|
|
194
247
|
|
|
195
|
-
response.status(
|
|
248
|
+
response.status(status)
|
|
249
|
+
response.setHeader('content-type', 'application/json')
|
|
250
|
+
response.send(JSON.stringify({ errors: [responseError] }))
|
|
196
251
|
return
|
|
197
252
|
}
|
|
198
253
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestError,
|
|
3
|
+
FastStoreError,
|
|
4
|
+
ForbiddenError,
|
|
5
|
+
NotFoundError,
|
|
6
|
+
UnauthorizedError,
|
|
7
|
+
} from '@faststore/api'
|
|
8
|
+
import { GraphQLError } from 'graphql'
|
|
9
|
+
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
|
|
12
|
+
import handler from '../../../src/pages/api/graphql'
|
|
13
|
+
import { execute } from '../../../src/server'
|
|
14
|
+
|
|
15
|
+
vi.mock('../../../src/server', () => ({
|
|
16
|
+
execute: vi.fn(),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
const mockedExecute = vi.mocked(execute)
|
|
20
|
+
|
|
21
|
+
const createResponse = () => {
|
|
22
|
+
const res = {
|
|
23
|
+
status: vi.fn().mockReturnThis(),
|
|
24
|
+
setHeader: vi.fn().mockReturnThis(),
|
|
25
|
+
send: vi.fn().mockReturnThis(),
|
|
26
|
+
end: vi.fn().mockReturnThis(),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return res as unknown as NextApiResponse & typeof res
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const createRequest = (operationName = 'ClientShippingSimulationQuery') =>
|
|
33
|
+
({
|
|
34
|
+
method: 'GET',
|
|
35
|
+
headers: { host: 'localhost:3000' },
|
|
36
|
+
query: {
|
|
37
|
+
operationName,
|
|
38
|
+
operationHash: 'hash',
|
|
39
|
+
variables: '{}',
|
|
40
|
+
},
|
|
41
|
+
}) as unknown as NextApiRequest
|
|
42
|
+
|
|
43
|
+
const mockExecuteWithErrors = (errors: unknown[]) => {
|
|
44
|
+
mockedExecute.mockResolvedValue({
|
|
45
|
+
data: null,
|
|
46
|
+
errors,
|
|
47
|
+
extensions: { cookies: new Map(), cacheControl: undefined },
|
|
48
|
+
} as never)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reproduces what `useMaskedErrors` produces: a GraphQLError whose
|
|
52
|
+
// `originalError` is the FastStoreError thrown by a resolver/`fetchAPI`.
|
|
53
|
+
const maskedError = (original: FastStoreError) =>
|
|
54
|
+
new GraphQLError(original.message, { originalError: original })
|
|
55
|
+
|
|
56
|
+
describe('/api/graphql error status propagation', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
vi.clearAllMocks()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.unstubAllEnvs()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('propagates the upstream 400 from a wrapped BadRequestError instead of 500', async () => {
|
|
66
|
+
mockExecuteWithErrors([
|
|
67
|
+
maskedError(new BadRequestError('O campo CEP (88) é inválido')),
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
const res = createResponse()
|
|
71
|
+
await handler(createRequest(), res)
|
|
72
|
+
|
|
73
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
74
|
+
expect(res.end).not.toHaveBeenCalled()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
['UnauthorizedError', () => new UnauthorizedError('nope'), 401],
|
|
79
|
+
['ForbiddenError', () => new ForbiddenError('nope'), 403],
|
|
80
|
+
['NotFoundError', () => new NotFoundError('nope'), 404],
|
|
81
|
+
])('propagates %s status', async (_name, makeError, expectedStatus) => {
|
|
82
|
+
mockExecuteWithErrors([maskedError(makeError())])
|
|
83
|
+
|
|
84
|
+
const res = createResponse()
|
|
85
|
+
await handler(createRequest(), res)
|
|
86
|
+
|
|
87
|
+
expect(res.status).toHaveBeenCalledWith(expectedStatus)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('propagates an unmapped upstream status (UnknownError 429)', async () => {
|
|
91
|
+
mockExecuteWithErrors([
|
|
92
|
+
maskedError(
|
|
93
|
+
new FastStoreError({ status: 429, type: 'UnknownError' }, 'slow down')
|
|
94
|
+
),
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
const res = createResponse()
|
|
98
|
+
await handler(createRequest(), res)
|
|
99
|
+
|
|
100
|
+
expect(res.status).toHaveBeenCalledWith(429)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('recovers status when the error itself is a FastStoreError (not wrapped)', async () => {
|
|
104
|
+
mockExecuteWithErrors([new BadRequestError('bad input')])
|
|
105
|
+
|
|
106
|
+
const res = createResponse()
|
|
107
|
+
await handler(createRequest(), res)
|
|
108
|
+
|
|
109
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('falls back to a body-less 500 for non-FastStore errors', async () => {
|
|
113
|
+
mockExecuteWithErrors([new GraphQLError('Sorry, something went wrong.')])
|
|
114
|
+
|
|
115
|
+
const res = createResponse()
|
|
116
|
+
await handler(createRequest(), res)
|
|
117
|
+
|
|
118
|
+
expect(res.status).toHaveBeenCalledWith(500)
|
|
119
|
+
expect(res.end).toHaveBeenCalled()
|
|
120
|
+
expect(res.send).not.toHaveBeenCalled()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('exposes type, status and message in the body outside production', async () => {
|
|
124
|
+
vi.stubEnv('NODE_ENV', 'development')
|
|
125
|
+
mockExecuteWithErrors([maskedError(new BadRequestError('invalid CEP'))])
|
|
126
|
+
|
|
127
|
+
const res = createResponse()
|
|
128
|
+
await handler(createRequest(), res)
|
|
129
|
+
|
|
130
|
+
const body = JSON.parse(
|
|
131
|
+
(res.send as ReturnType<typeof vi.fn>).mock.calls[0][0]
|
|
132
|
+
)
|
|
133
|
+
expect(body.errors[0]).toEqual({
|
|
134
|
+
extensions: { type: 'BadRequestError', status: 400 },
|
|
135
|
+
message: 'invalid CEP',
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('omits the free-text message from the body in production', async () => {
|
|
140
|
+
vi.stubEnv('NODE_ENV', 'production')
|
|
141
|
+
mockExecuteWithErrors([maskedError(new BadRequestError('invalid CEP'))])
|
|
142
|
+
|
|
143
|
+
const res = createResponse()
|
|
144
|
+
await handler(createRequest(), res)
|
|
145
|
+
|
|
146
|
+
const body = JSON.parse(
|
|
147
|
+
(res.send as ReturnType<typeof vi.fn>).mock.calls[0][0]
|
|
148
|
+
)
|
|
149
|
+
expect(body.errors[0]).toEqual({
|
|
150
|
+
extensions: { type: 'BadRequestError', status: 400 },
|
|
151
|
+
})
|
|
152
|
+
expect(body.errors[0].message).toBeUndefined()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('uses the first recoverable FastStoreError when several errors exist', async () => {
|
|
156
|
+
mockExecuteWithErrors([
|
|
157
|
+
new GraphQLError('plain error'),
|
|
158
|
+
maskedError(new NotFoundError('missing')),
|
|
159
|
+
maskedError(new BadRequestError('bad')),
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
const res = createResponse()
|
|
163
|
+
await handler(createRequest(), res)
|
|
164
|
+
|
|
165
|
+
expect(res.status).toHaveBeenCalledWith(404)
|
|
166
|
+
})
|
|
167
|
+
})
|