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.
@@ -85,14 +85,59 @@ export default class VelociousDatabaseDriversMysql extends Base{
85
85
  }
86
86
 
87
87
  /**
88
- * Disposes the physical MySQL session after each logical pool checkout.
89
- * MySQL exposes open-ended session state, so reconnecting is safer than trying
90
- * to enumerate and reset variables, temporary tables, prepared statements,
91
- * SQL modes, and other caller-controlled state.
92
- * @returns {Promise<void>} - Resolves after the physical session is closed.
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
- await this._close()
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
- // Do nothing for now...
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.getHeader("content-type")?.value?.match(/^multipart\/form-data;\s*boundary=(.+)$/i)
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
- this.postBody = Buffer.concat(this.postBodyBuffers).toString("utf8")
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.isFinite(size)) throw new Error(`Invalid chunk size: ${sizeToken}`)
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
- this.postBody = Buffer.from(this.chunkedBodyChars).toString("utf8")
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.getHeader("content-type")?.value?.startsWith("application/json")) {
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() }