@bakery-framework/cli 2.0.0-alpha.3 → 2.0.0-alpha.5
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/package.json +3 -3
- package/src/args.ts +62 -0
- package/src/dev.ts +4 -60
- package/src/index.ts +4 -23
- package/src/pipeline.ts +69 -0
- package/src/schema-hash.ts +70 -0
- package/src/worker.ts +11 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakery-framework/cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.5",
|
|
4
4
|
"description": "Bakery entry point and process supervision: mode dispatch, dev watcher, cluster.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bakery",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"bun": ">=1.3.14"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@bakery-framework/core": "^2.0.0-alpha.
|
|
42
|
+
"@bakery-framework/core": "^2.0.0-alpha.5"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@bakery-framework/orm": "^2.0.0-alpha.
|
|
45
|
+
"@bakery-framework/orm": "^2.0.0-alpha.5"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
48
48
|
"@bakery-framework/orm": {
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-line argument parsing for the `bakery` bin.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `index.ts` so it can be tested: that file is the mode
|
|
5
|
+
* dispatcher, and importing it boots a server. Nothing here reads
|
|
6
|
+
* `process.argv` — the caller passes the slice, which is also what lets a test
|
|
7
|
+
* state the argv it means.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Upper bound on the auto-detected worker count. */
|
|
11
|
+
export const MAX_AUTO_THREADS = 8
|
|
12
|
+
|
|
13
|
+
/** Fallback when `navigator.hardwareConcurrency` is unavailable. */
|
|
14
|
+
export const FALLBACK_THREADS = 4
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Worker count for a bare `--threads` / `-t`, with no number after it.
|
|
18
|
+
*
|
|
19
|
+
* Capped rather than uncapped: past ~8 the workers contend on the same
|
|
20
|
+
* loopback accept queue and the shared pool's atomics, and a 64-core CI box
|
|
21
|
+
* forking 64 servers is not what the flag means.
|
|
22
|
+
*/
|
|
23
|
+
export function autoThreadCount(): number {
|
|
24
|
+
return Math.min(
|
|
25
|
+
Math.max(1, navigator.hardwareConcurrency || FALLBACK_THREADS),
|
|
26
|
+
MAX_AUTO_THREADS,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* How many cluster workers `--threads` asks for, or `null` for "no cluster".
|
|
32
|
+
*
|
|
33
|
+
* `null` and `1` are different answers and `index.ts` branches on the
|
|
34
|
+
* difference: `null` takes the single-process path, `1` still goes through
|
|
35
|
+
* `handleThreadsMaster` with one worker.
|
|
36
|
+
*
|
|
37
|
+
* Both spellings of each flag are accepted (`--threads 4`, `--threads=4`, and
|
|
38
|
+
* the `-t` forms). A flag with a non-numeric or absent value is not an error —
|
|
39
|
+
* it falls back to `autoThreadCount()`, so `--threads --dev` asks for the
|
|
40
|
+
* default rather than rejecting. `Math.max(1, …)` is what keeps `--threads 0`
|
|
41
|
+
* from requesting a cluster with no workers in it.
|
|
42
|
+
*/
|
|
43
|
+
export function parseThreadsOption(args: string[]): number | null {
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i]
|
|
46
|
+
if (arg === '--threads' || arg === '-t') {
|
|
47
|
+
const next = args[i + 1]
|
|
48
|
+
if (next && /^\d+$/.test(next)) {
|
|
49
|
+
return Math.max(1, parseInt(next, 10))
|
|
50
|
+
}
|
|
51
|
+
return autoThreadCount()
|
|
52
|
+
}
|
|
53
|
+
if (arg.startsWith('--threads=') || arg.startsWith('-t=')) {
|
|
54
|
+
const val = arg.split('=')[1]
|
|
55
|
+
if (val && /^\d+$/.test(val)) {
|
|
56
|
+
return Math.max(1, parseInt(val, 10))
|
|
57
|
+
}
|
|
58
|
+
return autoThreadCount()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
package/src/dev.ts
CHANGED
|
@@ -37,66 +37,6 @@ try {
|
|
|
37
37
|
process.exit(1)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Hash everything the boot-time schema sync reads: the schema source files
|
|
42
|
-
* (resolved with the same probe order as orm/sync/load.ts — configured path,
|
|
43
|
-
* then the `orm/` folder layout, then a root `schema.ts`) plus the DB target,
|
|
44
|
-
* since switching `DB_URL` changes what "synced" means.
|
|
45
|
-
*
|
|
46
|
-
* Returns `null` for any indeterminate state — a configured path that does not
|
|
47
|
-
* exist, an unreadable file — so `classifySchemaSync` fails closed into
|
|
48
|
-
* re-syncing. Total absence of a schema is *not* indeterminate (it is a
|
|
49
|
-
* supported state for the defaults) and hashes to a stable value.
|
|
50
|
-
*/
|
|
51
|
-
async function computeSchemaHash(
|
|
52
|
-
configured: string | undefined,
|
|
53
|
-
): Promise<string | null> {
|
|
54
|
-
const { fs, Try } = await import('@bakery-framework/core/utils')
|
|
55
|
-
|
|
56
|
-
const files: string[] = []
|
|
57
|
-
const scanDir = async (dir: string) => {
|
|
58
|
-
for await (const file of new Bun.Glob('*.ts').scan({
|
|
59
|
-
cwd: dir,
|
|
60
|
-
absolute: true,
|
|
61
|
-
})) {
|
|
62
|
-
files.push(file)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const [error] = await Try.catch(
|
|
67
|
-
(async () => {
|
|
68
|
-
if (configured) {
|
|
69
|
-
const path = fs.resolve(fs.cwd, configured)
|
|
70
|
-
if (await fs.isDir(path)) {
|
|
71
|
-
await scanDir(path)
|
|
72
|
-
} else if (await Bun.file(path).exists()) {
|
|
73
|
-
files.push(path)
|
|
74
|
-
} else {
|
|
75
|
-
// Configured-but-missing is SyncService's SCHEMA_NOT_FOUND case:
|
|
76
|
-
// let the sync run and produce its proper error.
|
|
77
|
-
throw new Error(`configured schema path not found: ${path}`)
|
|
78
|
-
}
|
|
79
|
-
} else if (await Bun.file(`${fs.cwd}/orm/index.ts`).exists()) {
|
|
80
|
-
await scanDir(`${fs.cwd}/orm`)
|
|
81
|
-
} else if (await Bun.file(`${fs.cwd}/schema.ts`).exists()) {
|
|
82
|
-
files.push(`${fs.cwd}/schema.ts`)
|
|
83
|
-
}
|
|
84
|
-
})(),
|
|
85
|
-
)
|
|
86
|
-
if (error) return null
|
|
87
|
-
|
|
88
|
-
files.sort()
|
|
89
|
-
const hasher = new Bun.CryptoHasher('sha256')
|
|
90
|
-
hasher.update(process.env.DB_URL || process.env.DATABASE_URL || '')
|
|
91
|
-
for (const file of files) {
|
|
92
|
-
hasher.update(`\0${file}\0`)
|
|
93
|
-
const [readError, content] = await Try.catch(Bun.file(file).text())
|
|
94
|
-
if (readError) return null
|
|
95
|
-
hasher.update(content)
|
|
96
|
-
}
|
|
97
|
-
return hasher.digest('hex')
|
|
98
|
-
}
|
|
99
|
-
|
|
100
40
|
// The entire block is schema sync, so with no ORM there is nothing here to do.
|
|
101
41
|
// Silently: a dev boot of an app that never had a database should not report
|
|
102
42
|
// the absence of one on every reload.
|
|
@@ -108,6 +48,10 @@ if (hasORM()) {
|
|
|
108
48
|
)
|
|
109
49
|
const { schemaFromConfig } = await import('@bakery-framework/orm/sync/load')
|
|
110
50
|
const { Try } = await import('@bakery-framework/core/utils')
|
|
51
|
+
// Dynamic, like every other import in this block: `schema-hash.ts` is only
|
|
52
|
+
// needed when the ORM is installed, and a static import would pull it (and
|
|
53
|
+
// its own dynamic core barrel) onto the no-ORM boot path.
|
|
54
|
+
const { computeSchemaHash } = await import('./schema-hash')
|
|
111
55
|
|
|
112
56
|
const hashFile = `${Bakery.cacheDir}/schema-sync.hash`
|
|
113
57
|
const currentHash = await computeSchemaHash(schemaFromConfig(config))
|
package/src/index.ts
CHANGED
|
@@ -4,34 +4,15 @@ import '@bakery-framework/core/core/init'
|
|
|
4
4
|
// Safe as a static import: `core/port` reads `process.env` and imports nothing,
|
|
5
5
|
// so it cannot be the edge that closes core's barrel cycle.
|
|
6
6
|
import { applyPortFlag } from '@bakery-framework/core/core/port'
|
|
7
|
+
// Safe as a static import for the same reason as `core/port`: `args.ts` imports
|
|
8
|
+
// nothing at all.
|
|
9
|
+
import { parseThreadsOption } from './args'
|
|
7
10
|
|
|
8
11
|
const isDev = import.meta.env.DEV
|
|
9
12
|
const isDevWorker = import.meta.env.DEV_WORKER
|
|
10
13
|
const isThreadWorker = import.meta.env.THREAD_WORKER
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
const args = process.argv.slice(2)
|
|
14
|
-
for (let i = 0; i < args.length; i++) {
|
|
15
|
-
const arg = args[i]
|
|
16
|
-
if (arg === '--threads' || arg === '-t') {
|
|
17
|
-
const next = args[i + 1]
|
|
18
|
-
if (next && /^\d+$/.test(next)) {
|
|
19
|
-
return Math.max(1, parseInt(next, 10))
|
|
20
|
-
}
|
|
21
|
-
return Math.min(Math.max(1, navigator.hardwareConcurrency || 4), 8)
|
|
22
|
-
}
|
|
23
|
-
if (arg.startsWith('--threads=') || arg.startsWith('-t=')) {
|
|
24
|
-
const val = arg.split('=')[1]
|
|
25
|
-
if (val && /^\d+$/.test(val)) {
|
|
26
|
-
return Math.max(1, parseInt(val, 10))
|
|
27
|
-
}
|
|
28
|
-
return Math.min(Math.max(1, navigator.hardwareConcurrency || 4), 8)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const threadsOption = getThreadsOption()
|
|
15
|
+
const threadsOption = parseThreadsOption(process.argv.slice(2))
|
|
35
16
|
|
|
36
17
|
// Before any mode takes over, and before the config is read: `applyPortFlag`
|
|
37
18
|
// writes `process.env.PORT`, which is what the worker, the startup banner and
|
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { is } from '@bakery-framework/core/utils/common'
|
|
2
|
+
import { getClientIp } from '@bakery-framework/core/utils/http'
|
|
3
|
+
import { retryAfterSeconds } from './rate-limit'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The three decisions `worker.ts`'s `fetch` makes that are pure functions of
|
|
7
|
+
* their arguments.
|
|
8
|
+
*
|
|
9
|
+
* They were inline in the `Bun.serve` callback, which is unreachable from a
|
|
10
|
+
* test: `worker.ts` calls `Bun.serve` at module scope, so importing it binds a
|
|
11
|
+
* port. Nothing about them is server-specific, so they live here and the
|
|
12
|
+
* callback calls them — the serve options are otherwise unchanged.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The `rateLimit` config with the `false` (disabled) arm removed. */
|
|
16
|
+
export type RateLimitConfig = Exclude<ProcessedAppConfig['rateLimit'], false>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a handler's return value should be routed into the error registry.
|
|
20
|
+
*
|
|
21
|
+
* Two shapes count, and they are not interchangeable. A `Response` is an error
|
|
22
|
+
* by its status; anything else is an error by carrying `errorCode`, which is
|
|
23
|
+
* what `ErrorHandler.extractErrorData` reads.
|
|
24
|
+
*
|
|
25
|
+
* The `is.object` guard is load-bearing twice over. `'errorCode' in res` is a
|
|
26
|
+
* `TypeError` on a primitive, and `Try.return`'s failure sentinel is a
|
|
27
|
+
* `symbol` — so the guard is what keeps the rejection path from throwing
|
|
28
|
+
* inside the code that exists to handle throws. Note `is.object([])` is
|
|
29
|
+
* deliberately `true` (see CLAUDE.md); an array simply has no `errorCode`.
|
|
30
|
+
*
|
|
31
|
+
* 400 is included: `>= 400`, not `> 400`. A handler returning a bare
|
|
32
|
+
* `new Response(…, {status: 404})` must still reach `TSXErrorHandler` and get
|
|
33
|
+
* the app's error page rather than an empty body.
|
|
34
|
+
*/
|
|
35
|
+
export function isErrorResult(res: unknown): boolean {
|
|
36
|
+
if (res instanceof Response) return res.status >= 400
|
|
37
|
+
return is.object(res) && 'errorCode' in res
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Which token bucket this request spends from.
|
|
42
|
+
*
|
|
43
|
+
* The `|| hostname` is not a tidy-up. An empty key is a *valid* string that
|
|
44
|
+
* hashes to one fixed slot, so every client whose IP could not be determined —
|
|
45
|
+
* which is all of them when `Bakery.server` is not yet assigned, and any of
|
|
46
|
+
* them behind a proxy with `trustProxy` off — would share a single bucket and
|
|
47
|
+
* 429 each other. Falling back to the hostname keeps the collision at
|
|
48
|
+
* per-host, which is the coarsest grouping that is still meaningful.
|
|
49
|
+
*/
|
|
50
|
+
export function rateLimitKey(
|
|
51
|
+
rl: RateLimitConfig,
|
|
52
|
+
req: Request,
|
|
53
|
+
hostname: string,
|
|
54
|
+
): string {
|
|
55
|
+
return (rl.keyBy ? rl.keyBy(req) : getClientIp(req)) || hostname
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The 429 a rejected request receives.
|
|
60
|
+
*
|
|
61
|
+
* `Retry-After` is whole seconds per RFC 9110 and never 0 — see
|
|
62
|
+
* `retryAfterSeconds`.
|
|
63
|
+
*/
|
|
64
|
+
export function tooManyRequests(refill: number): Response {
|
|
65
|
+
return new Response('Too Many Requests', {
|
|
66
|
+
status: 429,
|
|
67
|
+
headers: { 'Retry-After': String(retryAfterSeconds(refill)) },
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash everything the boot-time schema sync reads: the schema source files
|
|
3
|
+
* (resolved with the same probe order as orm/sync/load.ts — configured path,
|
|
4
|
+
* then the `orm/` folder layout, then a root `schema.ts`) plus the DB target,
|
|
5
|
+
* since switching `DB_URL` changes what "synced" means.
|
|
6
|
+
*
|
|
7
|
+
* Returns `null` for any indeterminate state — a configured path that does not
|
|
8
|
+
* exist, an unreadable file — so `classifySchemaSync` fails closed into
|
|
9
|
+
* re-syncing. Total absence of a schema is *not* indeterminate (it is a
|
|
10
|
+
* supported state for the defaults) and hashes to a stable value.
|
|
11
|
+
*
|
|
12
|
+
* `base` defaults to the app's cwd, which is what `dev.ts` wants and the only
|
|
13
|
+
* value it passes. It is a parameter at all because `fs.cwd` is a module-level
|
|
14
|
+
* constant evaluated at import time, so a test cannot reach this function's
|
|
15
|
+
* probe any other way without `process.chdir` before the import — process-global
|
|
16
|
+
* state of exactly the kind convention 9 exists to keep out of the suite.
|
|
17
|
+
*
|
|
18
|
+
* The `@bakery-framework/core/utils` import stays dynamic, as it was inline in
|
|
19
|
+
* `dev.ts`: that barrel must not enter the module graph at `dev.ts` import time.
|
|
20
|
+
*/
|
|
21
|
+
export async function computeSchemaHash(
|
|
22
|
+
configured: string | undefined,
|
|
23
|
+
base?: string,
|
|
24
|
+
): Promise<string | null> {
|
|
25
|
+
const { fs, Try } = await import('@bakery-framework/core/utils')
|
|
26
|
+
const cwd = base ?? fs.cwd
|
|
27
|
+
|
|
28
|
+
const files: string[] = []
|
|
29
|
+
const scanDir = async (dir: string) => {
|
|
30
|
+
for await (const file of new Bun.Glob('*.ts').scan({
|
|
31
|
+
cwd: dir,
|
|
32
|
+
absolute: true,
|
|
33
|
+
})) {
|
|
34
|
+
files.push(file)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [error] = await Try.catch(
|
|
39
|
+
(async () => {
|
|
40
|
+
if (configured) {
|
|
41
|
+
const path = fs.resolve(cwd, configured)
|
|
42
|
+
if (await fs.isDir(path)) {
|
|
43
|
+
await scanDir(path)
|
|
44
|
+
} else if (await Bun.file(path).exists()) {
|
|
45
|
+
files.push(path)
|
|
46
|
+
} else {
|
|
47
|
+
// Configured-but-missing is SyncService's SCHEMA_NOT_FOUND case:
|
|
48
|
+
// let the sync run and produce its proper error.
|
|
49
|
+
throw new Error(`configured schema path not found: ${path}`)
|
|
50
|
+
}
|
|
51
|
+
} else if (await Bun.file(`${cwd}/orm/index.ts`).exists()) {
|
|
52
|
+
await scanDir(`${cwd}/orm`)
|
|
53
|
+
} else if (await Bun.file(`${cwd}/schema.ts`).exists()) {
|
|
54
|
+
files.push(`${cwd}/schema.ts`)
|
|
55
|
+
}
|
|
56
|
+
})(),
|
|
57
|
+
)
|
|
58
|
+
if (error) return null
|
|
59
|
+
|
|
60
|
+
files.sort()
|
|
61
|
+
const hasher = new Bun.CryptoHasher('sha256')
|
|
62
|
+
hasher.update(process.env.DB_URL || process.env.DATABASE_URL || '')
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
hasher.update(`\0${file}\0`)
|
|
65
|
+
const [readError, content] = await Try.catch(Bun.file(file).text())
|
|
66
|
+
if (readError) return null
|
|
67
|
+
hasher.update(content)
|
|
68
|
+
}
|
|
69
|
+
return hasher.digest('hex')
|
|
70
|
+
}
|
package/src/worker.ts
CHANGED
|
@@ -19,15 +19,12 @@ import {
|
|
|
19
19
|
} from '@bakery-framework/core/router'
|
|
20
20
|
import { Session } from '@bakery-framework/core/session'
|
|
21
21
|
import { runStartupBanner, setupServer } from '@bakery-framework/core/startup'
|
|
22
|
-
import { deferredValue,
|
|
23
|
-
import {
|
|
22
|
+
import { deferredValue, Try } from '@bakery-framework/core/utils/common'
|
|
23
|
+
import { parsedUrl } from '@bakery-framework/core/utils/http'
|
|
24
24
|
import { COUNTER_SLOTS } from '@bakery-framework/core/utils/shared-pool'
|
|
25
25
|
import { hasORM } from './orm'
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
retryAfterSeconds,
|
|
29
|
-
sampleRateLimitLog,
|
|
30
|
-
} from './rate-limit'
|
|
26
|
+
import { isErrorResult, rateLimitKey, tooManyRequests } from './pipeline'
|
|
27
|
+
import { rateLimitSlot, sampleRateLimitLog } from './rate-limit'
|
|
31
28
|
import { runShutdownSequence } from './shutdown'
|
|
32
29
|
|
|
33
30
|
/**
|
|
@@ -142,10 +139,10 @@ try {
|
|
|
142
139
|
maxRequestBodySize: Bakery.config.maxBodySize,
|
|
143
140
|
|
|
144
141
|
async fetch(req) {
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
// Memoized for the lifetime of the request: the router, body parser and
|
|
143
|
+
// proxy all want the same parse, and every one of them asks for it the
|
|
144
|
+
// same way.
|
|
145
|
+
const url = parsedUrl(req)
|
|
149
146
|
const hostname = getHostname(req)
|
|
150
147
|
const hostConfig = resolveHostConfig(hostname)
|
|
151
148
|
|
|
@@ -157,7 +154,7 @@ try {
|
|
|
157
154
|
|
|
158
155
|
const rl = Bakery.config.rateLimit
|
|
159
156
|
if (rl) {
|
|
160
|
-
const key = (rl
|
|
157
|
+
const key = rateLimitKey(rl, req, hostname)
|
|
161
158
|
const slot = rateLimitSlot(key)
|
|
162
159
|
if (!Bakery.sharedPool.consumeToken(slot, rl.max, rl.refill)) {
|
|
163
160
|
// Sampled — availability under flood: stdout is effectively
|
|
@@ -171,12 +168,7 @@ try {
|
|
|
171
168
|
serveLog.RATE_LIMITED({ ip: key })
|
|
172
169
|
}
|
|
173
170
|
}
|
|
174
|
-
return
|
|
175
|
-
status: 429,
|
|
176
|
-
headers: {
|
|
177
|
-
'Retry-After': String(retryAfterSeconds(rl.refill)),
|
|
178
|
-
},
|
|
179
|
-
})
|
|
171
|
+
return tooManyRequests(rl.refill)
|
|
180
172
|
}
|
|
181
173
|
}
|
|
182
174
|
|
|
@@ -186,10 +178,7 @@ try {
|
|
|
186
178
|
async function fetchHandler() {
|
|
187
179
|
const res = await handleRequest(req)
|
|
188
180
|
|
|
189
|
-
|
|
190
|
-
const isObjError = is.object(res) && 'errorCode' in res
|
|
191
|
-
|
|
192
|
-
if (isResError || isObjError) {
|
|
181
|
+
if (isErrorResult(res)) {
|
|
193
182
|
Bakery.sharedPool.incrementCounter(COUNTER_SLOTS.TOTAL_ERRORS, 1)
|
|
194
183
|
return await handleRequestError(path, req, res)
|
|
195
184
|
}
|