@naturalcycles/backend-lib 9.70.0 → 9.71.0
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.
|
@@ -64,6 +64,7 @@ export function compressionMiddleware(options) {
|
|
|
64
64
|
let length;
|
|
65
65
|
let listeners = [];
|
|
66
66
|
let stream;
|
|
67
|
+
let closed = false;
|
|
67
68
|
const _end = res.end;
|
|
68
69
|
const _on = res.on;
|
|
69
70
|
const _write = res.write;
|
|
@@ -126,6 +127,17 @@ export function compressionMiddleware(options) {
|
|
|
126
127
|
addListeners(res, _on, listeners);
|
|
127
128
|
listeners = null;
|
|
128
129
|
}
|
|
130
|
+
// Release the compression stream when the response closes, freeing native
|
|
131
|
+
// zlib memory even if the client disconnected before the response finished.
|
|
132
|
+
// Registered before onHeaders so a close that happens before the stream
|
|
133
|
+
// exists is not missed (the `closed` flag below handles a stream created
|
|
134
|
+
// afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
|
|
135
|
+
// We use `stream.destroy()` (not the `destroy` npm package upstream added)
|
|
136
|
+
// because our Node target frees the zlib handle on destroy().
|
|
137
|
+
_on.call(res, 'close', function onResponseClose() {
|
|
138
|
+
closed = true;
|
|
139
|
+
stream?.destroy();
|
|
140
|
+
});
|
|
129
141
|
onHeaders(res, function onResponseHeaders() {
|
|
130
142
|
// determine if request is filtered
|
|
131
143
|
if (!filter(req, res)) {
|
|
@@ -184,6 +196,14 @@ export function compressionMiddleware(options) {
|
|
|
184
196
|
else {
|
|
185
197
|
stream = zlib.createDeflate(zlibOpts);
|
|
186
198
|
}
|
|
199
|
+
// The response already closed before the stream was created, so the close
|
|
200
|
+
// listener above has already run. Release the stream now and drop the
|
|
201
|
+
// reference so later writes fall back to the raw response.
|
|
202
|
+
if (closed) {
|
|
203
|
+
stream.destroy();
|
|
204
|
+
stream = undefined;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
187
207
|
// add buffered listeners to stream
|
|
188
208
|
addListeners(stream, stream.on.bind(stream), listeners);
|
|
189
209
|
// header fields
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
+
import { _isPlainObject } from '@naturalcycles/js-lib';
|
|
3
|
+
import { _anyToErrorObject } from '@naturalcycles/js-lib/error';
|
|
4
|
+
import { _safeJsonStringify } from '@naturalcycles/js-lib/string/safeJsonStringify.js';
|
|
2
5
|
import { _objectAssign } from '@naturalcycles/js-lib/types';
|
|
3
6
|
import { _inspect } from '@naturalcycles/nodejs-lib';
|
|
4
7
|
import { dimGrey } from '@naturalcycles/nodejs-lib/colors';
|
|
@@ -41,17 +44,46 @@ export const ciLogger = {
|
|
|
41
44
|
// Documented here: https://cloud.google.com/logging/docs/structured-logging
|
|
42
45
|
// Cloud Run logging: https://cloud.google.com/run/docs/logging
|
|
43
46
|
function writeGCPStructuredLog(meta, args) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (args.length !== 1 || !_isPlainObject(args[0])) {
|
|
48
|
+
console.log(JSON.stringify({
|
|
49
|
+
message: args.map(a => (typeof a === 'string' ? a : inspect(a))).join(' '),
|
|
50
|
+
...meta,
|
|
51
|
+
}));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const { msg, err, ...entry } = args[0];
|
|
55
|
+
if (err instanceof Error) {
|
|
56
|
+
entry['err'] = _anyToErrorObject(err);
|
|
57
|
+
entry['message'] = msg ? `${msg}\n${inspect(err)}` : inspect(err);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
if (err !== undefined)
|
|
61
|
+
entry['err'] = err;
|
|
62
|
+
if (msg !== undefined)
|
|
63
|
+
entry['message'] = msg;
|
|
64
|
+
}
|
|
65
|
+
console.log(_safeJsonStringify({ ...entry, ...meta }));
|
|
48
66
|
}
|
|
49
67
|
function logToDev(requestId, args) {
|
|
50
68
|
// Run on local machine
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
if (args.length !== 1 || !_isPlainObject(args[0])) {
|
|
70
|
+
console.log([
|
|
71
|
+
requestId ? [dimGrey(`[${requestId}]`)] : [],
|
|
72
|
+
...args.map(a => _inspect(a, { includeErrorStack: true, colors: true })),
|
|
73
|
+
].join(' '));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const { msg, err, ...rest } = args[0];
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (requestId)
|
|
79
|
+
parts.push(dimGrey(`[${requestId}]`));
|
|
80
|
+
if (msg !== undefined)
|
|
81
|
+
parts.push(String(msg));
|
|
82
|
+
if (err !== undefined)
|
|
83
|
+
parts.push(_inspect(err, { includeErrorStack: true, colors: true }));
|
|
84
|
+
if (Object.keys(rest).length)
|
|
85
|
+
parts.push(dimGrey(_inspect(rest, { colors: false })));
|
|
86
|
+
console.log(parts.join(' '));
|
|
55
87
|
}
|
|
56
88
|
/**
|
|
57
89
|
* Same as logToDev, but without request and without colors.
|
package/package.json
CHANGED
|
@@ -113,6 +113,7 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
|
|
|
113
113
|
let length: number | undefined
|
|
114
114
|
let listeners: [string, (...args: any[]) => void][] | null = []
|
|
115
115
|
let stream: zlib.Gzip | zlib.Deflate | zlib.BrotliCompress | zlib.ZstdCompress | undefined
|
|
116
|
+
let closed = false
|
|
116
117
|
|
|
117
118
|
const _end = res.end
|
|
118
119
|
const _on = res.on
|
|
@@ -199,6 +200,18 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
|
|
|
199
200
|
listeners = null
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
// Release the compression stream when the response closes, freeing native
|
|
204
|
+
// zlib memory even if the client disconnected before the response finished.
|
|
205
|
+
// Registered before onHeaders so a close that happens before the stream
|
|
206
|
+
// exists is not missed (the `closed` flag below handles a stream created
|
|
207
|
+
// afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
|
|
208
|
+
// We use `stream.destroy()` (not the `destroy` npm package upstream added)
|
|
209
|
+
// because our Node target frees the zlib handle on destroy().
|
|
210
|
+
_on.call(res, 'close', function onResponseClose() {
|
|
211
|
+
closed = true
|
|
212
|
+
stream?.destroy()
|
|
213
|
+
})
|
|
214
|
+
|
|
202
215
|
onHeaders(res, function onResponseHeaders() {
|
|
203
216
|
// determine if request is filtered
|
|
204
217
|
if (!filter(req, res)) {
|
|
@@ -268,6 +281,15 @@ export function compressionMiddleware(options?: CompressionOptions): BackendRequ
|
|
|
268
281
|
stream = zlib.createDeflate(zlibOpts)
|
|
269
282
|
}
|
|
270
283
|
|
|
284
|
+
// The response already closed before the stream was created, so the close
|
|
285
|
+
// listener above has already run. Release the stream now and drop the
|
|
286
|
+
// reference so later writes fall back to the raw response.
|
|
287
|
+
if (closed) {
|
|
288
|
+
stream.destroy()
|
|
289
|
+
stream = undefined
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
271
293
|
// add buffered listeners to stream
|
|
272
294
|
addListeners(stream, stream.on.bind(stream), listeners!)
|
|
273
295
|
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EventLoopMonitorOptions,
|
|
3
|
-
EventLoopUtilization,
|
|
4
|
-
IntervalHistogram,
|
|
5
|
-
} from 'node:perf_hooks'
|
|
1
|
+
import type { ELDHistogram, EventLoopMonitorOptions, EventLoopUtilization } from 'node:perf_hooks'
|
|
6
2
|
import { monitorEventLoopDelay, performance, PerformanceObserver } from 'node:perf_hooks'
|
|
7
3
|
import type {
|
|
8
4
|
Integer,
|
|
@@ -45,7 +41,7 @@ export class EventLoopMonitor {
|
|
|
45
41
|
constructor(cfg: EventLoopMonitorCfg = {}) {
|
|
46
42
|
const { measureInterval = 60_000 } = cfg
|
|
47
43
|
|
|
48
|
-
const eldOptions:
|
|
44
|
+
const eldOptions: EventLoopMonitorOptions = {
|
|
49
45
|
// Node >=24.19 / >=26.5: sample once per event loop iteration (uv_prepare/uv_check hooks),
|
|
50
46
|
// which measures actual per-iteration delay instead of timer jitter.
|
|
51
47
|
// More accurate (no `resolution` offset baked into every percentile) and cheaper.
|
|
@@ -103,7 +99,7 @@ export class EventLoopMonitor {
|
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
private interval: NodeJS.Timeout
|
|
106
|
-
private eld:
|
|
102
|
+
private eld: ELDHistogram
|
|
107
103
|
private lastElu: EventLoopUtilization
|
|
108
104
|
/**
|
|
109
105
|
* Undefined until the first interval has completed.
|
|
@@ -167,11 +163,3 @@ export interface EventLoopStats {
|
|
|
167
163
|
*/
|
|
168
164
|
gcCPU: Integer
|
|
169
165
|
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Extends the Node types with `samplePerIteration` (added in Node 24.19 / 26.5).
|
|
173
|
-
* Remove once @types/node declares it.
|
|
174
|
-
*/
|
|
175
|
-
interface EventLoopMonitorOptions2 extends EventLoopMonitorOptions {
|
|
176
|
-
samplePerIteration?: boolean
|
|
177
|
-
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { inspect } from 'node:util'
|
|
2
|
+
import { _isPlainObject } from '@naturalcycles/js-lib'
|
|
3
|
+
import { _anyToErrorObject } from '@naturalcycles/js-lib/error'
|
|
2
4
|
import type { CommonLogger } from '@naturalcycles/js-lib/log'
|
|
5
|
+
import { _safeJsonStringify } from '@naturalcycles/js-lib/string/safeJsonStringify.js'
|
|
3
6
|
import { _objectAssign } from '@naturalcycles/js-lib/types'
|
|
4
7
|
import type { AnyObject } from '@naturalcycles/js-lib/types'
|
|
5
8
|
import { _inspect } from '@naturalcycles/nodejs-lib'
|
|
@@ -50,22 +53,46 @@ export const ciLogger: CommonLogger = {
|
|
|
50
53
|
// Documented here: https://cloud.google.com/logging/docs/structured-logging
|
|
51
54
|
// Cloud Run logging: https://cloud.google.com/run/docs/logging
|
|
52
55
|
function writeGCPStructuredLog(meta: AnyObject, args: any[]): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
if (args.length !== 1 || !_isPlainObject(args[0])) {
|
|
57
|
+
console.log(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
message: args.map(a => (typeof a === 'string' ? a : inspect(a))).join(' '),
|
|
60
|
+
...meta,
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { msg, err, ...entry } = args[0]
|
|
67
|
+
if (err instanceof Error) {
|
|
68
|
+
entry['err'] = _anyToErrorObject(err)
|
|
69
|
+
entry['message'] = msg ? `${msg}\n${inspect(err)}` : inspect(err)
|
|
70
|
+
} else {
|
|
71
|
+
if (err !== undefined) entry['err'] = err
|
|
72
|
+
if (msg !== undefined) entry['message'] = msg
|
|
73
|
+
}
|
|
74
|
+
console.log(_safeJsonStringify({ ...entry, ...meta }))
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
function logToDev(requestId: string | null, args: any[]): void {
|
|
62
78
|
// Run on local machine
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
if (args.length !== 1 || !_isPlainObject(args[0])) {
|
|
80
|
+
console.log(
|
|
81
|
+
[
|
|
82
|
+
requestId ? [dimGrey(`[${requestId}]`)] : [],
|
|
83
|
+
...args.map(a => _inspect(a, { includeErrorStack: true, colors: true })),
|
|
84
|
+
].join(' '),
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { msg, err, ...rest } = args[0]
|
|
90
|
+
const parts: string[] = []
|
|
91
|
+
if (requestId) parts.push(dimGrey(`[${requestId}]`))
|
|
92
|
+
if (msg !== undefined) parts.push(String(msg))
|
|
93
|
+
if (err !== undefined) parts.push(_inspect(err, { includeErrorStack: true, colors: true }))
|
|
94
|
+
if (Object.keys(rest).length) parts.push(dimGrey(_inspect(rest, { colors: false })))
|
|
95
|
+
console.log(parts.join(' '))
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
/**
|