velocious 1.0.678 → 1.0.680
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/README.md +9 -2
- package/build/configuration-types.js +22 -0
- package/build/configuration.js +42 -0
- package/build/database/drivers/mysql/index.js +51 -6
- package/build/http-server/client/request-buffer/index.js +59 -7
- package/build/http-server/client/request.js +5 -0
- package/build/src/configuration-types.d.ts +57 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +19 -1
- package/build/src/configuration.d.ts +8 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +36 -1
- package/build/src/database/drivers/mysql/index.d.ts +12 -5
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +55 -7
- package/build/src/http-server/client/request-buffer/index.d.ts +12 -2
- package/build/src/http-server/client/request-buffer/index.d.ts.map +1 -1
- package/build/src/http-server/client/request-buffer/index.js +59 -8
- package/build/src/http-server/client/request.d.ts +5 -0
- package/build/src/http-server/client/request.d.ts.map +1 -1
- package/build/src/http-server/client/request.js +6 -1
- package/package.json +3 -3
- package/src/configuration-types.js +22 -0
- package/src/configuration.js +42 -0
- package/src/database/drivers/mysql/index.js +51 -6
- package/src/http-server/client/request-buffer/index.js +59 -7
- package/src/http-server/client/request.js +5 -0
|
@@ -85,14 +85,59 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
88
|
+
* Resets the MySQL session state after each logical pool checkout while
|
|
89
|
+
* reusing the existing physical connection.
|
|
90
|
+
* MySQL exposes open-ended session state (user variables, temporary tables,
|
|
91
|
+
* prepared statements, `SET SESSION` changes), so the state must be cleared
|
|
92
|
+
* before the logical pool entry is handed out again. `COM_CHANGE_USER`
|
|
93
|
+
* performs a full session re-initialization on the server side — the same
|
|
94
|
+
* state a fresh handshake would start with — without opening a new TCP
|
|
95
|
+
* connection. That keeps checkouts isolated from each other while avoiding
|
|
96
|
+
* a reconnect (handshake + auth + schema re-introspection) on every
|
|
97
|
+
* operation. If the reset fails the physical session is closed instead, so
|
|
98
|
+
* the next query reconnects on a fresh session as a safe fallback.
|
|
99
|
+
* @returns {Promise<void>} - Resolves once the session state is reset.
|
|
93
100
|
*/
|
|
94
101
|
async cleanupSessionStateAfterCheckout() {
|
|
95
|
-
|
|
102
|
+
const pool = this.pool
|
|
103
|
+
|
|
104
|
+
if (!pool) return
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const pooledConnection = await new Promise((resolve, reject) => {
|
|
108
|
+
pool.getConnection((error, connection) => {
|
|
109
|
+
if (error) reject(error instanceof Error ? error : new Error(`Failed to check out connection for session reset: ${error}`))
|
|
110
|
+
else resolve(connection)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
pooledConnection.changeUser({charset: "utf8mb4", timeout: 10000}, ((/** @type {import("mysql").MysqlError} */ error) => {
|
|
117
|
+
if (error) reject(error instanceof Error ? error : new Error(`MySQL session reset failed: ${error}`))
|
|
118
|
+
else resolve(undefined)
|
|
119
|
+
}))
|
|
120
|
+
})
|
|
121
|
+
} finally {
|
|
122
|
+
pooledConnection.release()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The server re-initialized the session on this socket: user variables,
|
|
126
|
+
// temporary tables, prepared statements, and session variables are gone,
|
|
127
|
+
// and the session time zone must be re-established before the next query.
|
|
128
|
+
this.resetCurrentSessionTimeZone()
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// A failed reset leaves the session state unknown, so fall back to a
|
|
131
|
+
// full physical disconnect: the next query reconnects on a fresh session.
|
|
132
|
+
// The logical connection is still reusable afterwards.
|
|
133
|
+
try {
|
|
134
|
+
await this._close()
|
|
135
|
+
} catch (closeError) {
|
|
136
|
+
throw new AggregateError([error, closeError], "MySQL session reset failed and the physical disconnect fallback also failed", {cause: closeError})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw error
|
|
140
|
+
}
|
|
96
141
|
}
|
|
97
142
|
|
|
98
143
|
/**
|
|
@@ -31,6 +31,14 @@ function truncatePreview(input, limit = 300) {
|
|
|
31
31
|
export default class RequestBuffer {
|
|
32
32
|
bodyLength = 0
|
|
33
33
|
|
|
34
|
+
/** @type {import("../../../configuration-types.js").ResolvedHttpRequestBodyPolicy | undefined} */
|
|
35
|
+
requestBodyPolicy = undefined
|
|
36
|
+
|
|
37
|
+
/** @type {Buffer | undefined} */
|
|
38
|
+
rawBodyBuffer = undefined
|
|
39
|
+
|
|
40
|
+
destroyed = false
|
|
41
|
+
|
|
34
42
|
/** @type {Buffer[] | undefined} */
|
|
35
43
|
postBodyBuffers = undefined
|
|
36
44
|
|
|
@@ -73,7 +81,7 @@ export default class RequestBuffer {
|
|
|
73
81
|
* @returns {void}
|
|
74
82
|
*/
|
|
75
83
|
assertRequestBodySize(actualBytes) {
|
|
76
|
-
const maxBytes = this.configuration.getHttpServerMaxRequestBodyBytes()
|
|
84
|
+
const maxBytes = this.requestBodyPolicy?.maxRequestBodyBytes ?? this.configuration.getHttpServerMaxRequestBodyBytes()
|
|
77
85
|
|
|
78
86
|
if (maxBytes !== undefined && actualBytes > maxBytes) {
|
|
79
87
|
throw new HttpRequestBodyTooLargeError({actualBytes, maxBytes})
|
|
@@ -93,7 +101,25 @@ export default class RequestBuffer {
|
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
destroy() {
|
|
96
|
-
|
|
104
|
+
this.destroyed = true
|
|
105
|
+
this.rawBodyBuffer = undefined
|
|
106
|
+
this.postBody = undefined
|
|
107
|
+
this.postBodyBuffers = undefined
|
|
108
|
+
this.chunkedBodyChars = undefined
|
|
109
|
+
this.data = []
|
|
110
|
+
this.formDataPart = undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns exact request body bytes for a completed request whose policy selected raw mode.
|
|
115
|
+
* @returns {Buffer} - A copy of the exact request body bytes.
|
|
116
|
+
*/
|
|
117
|
+
getRawBody() {
|
|
118
|
+
if (this.destroyed) throw new Error("Raw request body is unavailable after request cleanup")
|
|
119
|
+
if (this.requestBodyPolicy?.mode !== "raw") throw new Error("Raw request body is available only when the request body policy selects raw mode")
|
|
120
|
+
if (!this.completed || !this.rawBodyBuffer) throw new Error("Raw request body is unavailable before request parsing completes")
|
|
121
|
+
|
|
122
|
+
return Buffer.from(this.rawBodyBuffer)
|
|
97
123
|
}
|
|
98
124
|
|
|
99
125
|
/**
|
|
@@ -210,6 +236,7 @@ export default class RequestBuffer {
|
|
|
210
236
|
if (this.currentChunkSize === undefined) throw new Error("Chunk size not initialized")
|
|
211
237
|
if (!chunkedBodyChars) throw new Error("Chunked body not initialized")
|
|
212
238
|
|
|
239
|
+
this.assertRequestBodySize(chunkedBodyChars.length + 1)
|
|
213
240
|
chunkedBodyChars.push(char)
|
|
214
241
|
/**
|
|
215
242
|
* Current chunk bytes read.
|
|
@@ -429,6 +456,13 @@ export default class RequestBuffer {
|
|
|
429
456
|
const httpMethod = this.httpMethod?.toUpperCase()
|
|
430
457
|
|
|
431
458
|
if (!httpMethod) throw new Error("HTTP method not set")
|
|
459
|
+
if (!this.path) throw new Error("HTTP path not set")
|
|
460
|
+
|
|
461
|
+
this.requestBodyPolicy = this.configuration.resolveHttpRequestBodyPolicy({
|
|
462
|
+
headers: this.getHeadersHash(),
|
|
463
|
+
httpMethod,
|
|
464
|
+
path: this.path
|
|
465
|
+
})
|
|
432
466
|
|
|
433
467
|
if (!this.expectsRequestBody(httpMethod)) {
|
|
434
468
|
this.completeRequest()
|
|
@@ -446,7 +480,9 @@ export default class RequestBuffer {
|
|
|
446
480
|
this.assertRequestBodySize(this.contentLength)
|
|
447
481
|
}
|
|
448
482
|
|
|
449
|
-
const match = this.
|
|
483
|
+
const match = this.requestBodyPolicy.mode === "parsed"
|
|
484
|
+
? this.getHeader("content-type")?.value?.match(/^multipart\/form-data;\s*boundary=(.+)$/i)
|
|
485
|
+
: null
|
|
450
486
|
|
|
451
487
|
if (match) {
|
|
452
488
|
this.boundary = match[1]
|
|
@@ -490,7 +526,13 @@ export default class RequestBuffer {
|
|
|
490
526
|
|
|
491
527
|
postRequestDone() {
|
|
492
528
|
if (this.postBodyBuffers) {
|
|
493
|
-
|
|
529
|
+
const bodyBuffer = Buffer.concat(this.postBodyBuffers)
|
|
530
|
+
|
|
531
|
+
if (this.requestBodyPolicy?.mode === "raw") {
|
|
532
|
+
this.rawBodyBuffer = bodyBuffer
|
|
533
|
+
} else {
|
|
534
|
+
this.postBody = bodyBuffer.toString("utf8")
|
|
535
|
+
}
|
|
494
536
|
}
|
|
495
537
|
|
|
496
538
|
this.postBodyBuffers = undefined
|
|
@@ -542,9 +584,11 @@ export default class RequestBuffer {
|
|
|
542
584
|
|
|
543
585
|
if (!sizeToken) throw new Error(`Invalid chunk size line: ${line}`)
|
|
544
586
|
|
|
587
|
+
if (!/^[0-9a-f]+$/iu.test(sizeToken)) throw new Error(`Invalid chunk size: ${sizeToken}`)
|
|
588
|
+
|
|
545
589
|
const size = Number.parseInt(sizeToken, 16)
|
|
546
590
|
|
|
547
|
-
if (!Number.
|
|
591
|
+
if (!Number.isSafeInteger(size)) throw new Error(`Invalid chunk size: ${sizeToken}`)
|
|
548
592
|
|
|
549
593
|
if (size === 0) {
|
|
550
594
|
this.setState("chunked-trailer")
|
|
@@ -564,7 +608,13 @@ export default class RequestBuffer {
|
|
|
564
608
|
*/
|
|
565
609
|
finishChunkedBody() {
|
|
566
610
|
if (this.chunkedBodyChars) {
|
|
567
|
-
|
|
611
|
+
const bodyBuffer = Buffer.from(this.chunkedBodyChars)
|
|
612
|
+
|
|
613
|
+
if (this.requestBodyPolicy?.mode === "raw") {
|
|
614
|
+
this.rawBodyBuffer = bodyBuffer
|
|
615
|
+
} else {
|
|
616
|
+
this.postBody = bodyBuffer.toString("utf8")
|
|
617
|
+
}
|
|
568
618
|
}
|
|
569
619
|
|
|
570
620
|
delete this.chunkedBodyChars
|
|
@@ -584,7 +634,9 @@ export default class RequestBuffer {
|
|
|
584
634
|
this.state = "status" // Reset state to new request
|
|
585
635
|
this.completed = true
|
|
586
636
|
|
|
587
|
-
if (this.
|
|
637
|
+
if (this.requestBodyPolicy?.mode === "raw") {
|
|
638
|
+
this.rawBodyBuffer ||= Buffer.alloc(0)
|
|
639
|
+
} else if (this.getHeader("content-type")?.value?.startsWith("application/json")) {
|
|
588
640
|
this.parseApplicationJsonParams()
|
|
589
641
|
} else if (this.multiPartyFormData) {
|
|
590
642
|
// Done after each new form data part
|
|
@@ -107,6 +107,11 @@ export default class VelociousHttpServerClientRequest {
|
|
|
107
107
|
socketRemoteAddress: this.socketRemoteAddress()
|
|
108
108
|
})
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Returns exact body bytes for requests whose header-stage body policy selected raw mode.
|
|
112
|
+
* @returns {Buffer} - A copy of the exact request body bytes.
|
|
113
|
+
*/
|
|
114
|
+
rawBody() { return this.getRequestBuffer().getRawBody() }
|
|
110
115
|
socketRemoteAddress() { return this.client?.remoteAddress }
|
|
111
116
|
|
|
112
117
|
getRequestBuffer() { return this.getRequestParser().getRequestBuffer() }
|