@durable-streams/server 0.1.6 → 0.2.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.
- package/dist/index.cjs +21 -9
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -9
- package/package.json +4 -4
- package/src/registry-hook.ts +2 -2
- package/src/server.ts +46 -9
package/dist/index.cjs
CHANGED
|
@@ -1369,10 +1369,14 @@ const CURSOR_QUERY_PARAM = `cursor`;
|
|
|
1369
1369
|
* Line terminators in the payload (CR, LF, or CRLF) become separate data: lines.
|
|
1370
1370
|
* This prevents CRLF injection attacks where malicious payloads could inject
|
|
1371
1371
|
* fake SSE events using CR-only line terminators.
|
|
1372
|
+
*
|
|
1373
|
+
* Note: We don't add a space after "data:" because clients strip exactly one
|
|
1374
|
+
* leading space per the SSE spec. Adding one would cause data starting with
|
|
1375
|
+
* spaces to lose an extra space character.
|
|
1372
1376
|
*/
|
|
1373
1377
|
function encodeSSEData(payload) {
|
|
1374
1378
|
const lines = payload.split(/\r\n|\r|\n/);
|
|
1375
|
-
return lines.map((line) => `data
|
|
1379
|
+
return lines.map((line) => `data:${line}`).join(`\n`) + `\n\n`;
|
|
1376
1380
|
}
|
|
1377
1381
|
/**
|
|
1378
1382
|
* Minimum response size to consider for compression.
|
|
@@ -1554,10 +1558,12 @@ var DurableStreamTestServer = class {
|
|
|
1554
1558
|
if (fault.truncateBodyBytes !== void 0 && modified.length > fault.truncateBodyBytes) modified = modified.slice(0, fault.truncateBodyBytes);
|
|
1555
1559
|
if (fault.corruptBody && modified.length > 0) {
|
|
1556
1560
|
modified = new Uint8Array(modified);
|
|
1557
|
-
|
|
1561
|
+
modified[0] = 88;
|
|
1562
|
+
if (modified.length > 1) modified[1] = 89;
|
|
1563
|
+
const numCorrupt = Math.max(1, Math.floor(modified.length * .1));
|
|
1558
1564
|
for (let i = 0; i < numCorrupt; i++) {
|
|
1559
1565
|
const pos = Math.floor(Math.random() * modified.length);
|
|
1560
|
-
modified[pos] =
|
|
1566
|
+
modified[pos] = 90;
|
|
1561
1567
|
}
|
|
1562
1568
|
}
|
|
1563
1569
|
return modified;
|
|
@@ -1595,7 +1601,7 @@ var DurableStreamTestServer = class {
|
|
|
1595
1601
|
res.end(`Injected error for testing`);
|
|
1596
1602
|
return;
|
|
1597
1603
|
}
|
|
1598
|
-
if (fault.truncateBodyBytes !== void 0 || fault.corruptBody) res._injectedFault = fault;
|
|
1604
|
+
if (fault.truncateBodyBytes !== void 0 || fault.corruptBody || fault.injectSseEvent) res._injectedFault = fault;
|
|
1599
1605
|
}
|
|
1600
1606
|
try {
|
|
1601
1607
|
switch (method) {
|
|
@@ -1836,6 +1842,11 @@ var DurableStreamTestServer = class {
|
|
|
1836
1842
|
"x-content-type-options": `nosniff`,
|
|
1837
1843
|
"cross-origin-resource-policy": `cross-origin`
|
|
1838
1844
|
});
|
|
1845
|
+
const fault = res._injectedFault;
|
|
1846
|
+
if (fault?.injectSseEvent) {
|
|
1847
|
+
res.write(`event: ${fault.injectSseEvent.eventType}\n`);
|
|
1848
|
+
res.write(`data: ${fault.injectSseEvent.data}\n\n`);
|
|
1849
|
+
}
|
|
1839
1850
|
let currentOffset = initialOffset;
|
|
1840
1851
|
let isConnected = true;
|
|
1841
1852
|
const decoder = new TextDecoder();
|
|
@@ -2030,10 +2041,10 @@ var DurableStreamTestServer = class {
|
|
|
2030
2041
|
res.end(`Missing required field: path`);
|
|
2031
2042
|
return;
|
|
2032
2043
|
}
|
|
2033
|
-
const hasFaultType = config.status !== void 0 || config.delayMs !== void 0 || config.dropConnection || config.truncateBodyBytes !== void 0 || config.corruptBody;
|
|
2044
|
+
const hasFaultType = config.status !== void 0 || config.delayMs !== void 0 || config.dropConnection || config.truncateBodyBytes !== void 0 || config.corruptBody || config.injectSseEvent !== void 0;
|
|
2034
2045
|
if (!hasFaultType) {
|
|
2035
2046
|
res.writeHead(400, { "content-type": `text/plain` });
|
|
2036
|
-
res.end(`Must specify at least one fault type: status, delayMs, dropConnection, truncateBodyBytes, or
|
|
2047
|
+
res.end(`Must specify at least one fault type: status, delayMs, dropConnection, truncateBodyBytes, corruptBody, or injectSseEvent`);
|
|
2037
2048
|
return;
|
|
2038
2049
|
}
|
|
2039
2050
|
this.injectFault(config.path, {
|
|
@@ -2046,7 +2057,8 @@ var DurableStreamTestServer = class {
|
|
|
2046
2057
|
probability: config.probability,
|
|
2047
2058
|
method: config.method,
|
|
2048
2059
|
corruptBody: config.corruptBody,
|
|
2049
|
-
jitterMs: config.jitterMs
|
|
2060
|
+
jitterMs: config.jitterMs,
|
|
2061
|
+
injectSseEvent: config.injectSseEvent
|
|
2050
2062
|
});
|
|
2051
2063
|
res.writeHead(200, { "content-type": `application/json` });
|
|
2052
2064
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -2128,13 +2140,13 @@ function createRegistryHooks(store, serverUrl) {
|
|
|
2128
2140
|
createdAt: event.timestamp
|
|
2129
2141
|
}
|
|
2130
2142
|
});
|
|
2131
|
-
await registryStream.append(changeEvent);
|
|
2143
|
+
await registryStream.append(JSON.stringify(changeEvent));
|
|
2132
2144
|
},
|
|
2133
2145
|
onStreamDeleted: async (event) => {
|
|
2134
2146
|
await ensureRegistryExists();
|
|
2135
2147
|
const streamName = extractStreamName(event.path);
|
|
2136
2148
|
const changeEvent = registryStateSchema.streams.delete({ key: streamName });
|
|
2137
|
-
await registryStream.append(changeEvent);
|
|
2149
|
+
await registryStream.append(JSON.stringify(changeEvent));
|
|
2138
2150
|
}
|
|
2139
2151
|
};
|
|
2140
2152
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -490,6 +490,13 @@ interface InjectedFault {
|
|
|
490
490
|
corruptBody?: boolean;
|
|
491
491
|
/** Add jitter to delay (random 0-jitterMs added to delayMs) */
|
|
492
492
|
jitterMs?: number;
|
|
493
|
+
/** Inject an SSE event with custom type and data (for testing SSE parsing) */
|
|
494
|
+
injectSseEvent?: {
|
|
495
|
+
/** Event type (e.g., "unknown", "control", "data") */
|
|
496
|
+
eventType: string;
|
|
497
|
+
/** Event data (will be sent as-is) */
|
|
498
|
+
data: string;
|
|
499
|
+
};
|
|
493
500
|
}
|
|
494
501
|
declare class DurableStreamTestServer {
|
|
495
502
|
readonly store: StreamStore | FileBackedStreamStore;
|
package/dist/index.d.ts
CHANGED
|
@@ -490,6 +490,13 @@ interface InjectedFault {
|
|
|
490
490
|
corruptBody?: boolean;
|
|
491
491
|
/** Add jitter to delay (random 0-jitterMs added to delayMs) */
|
|
492
492
|
jitterMs?: number;
|
|
493
|
+
/** Inject an SSE event with custom type and data (for testing SSE parsing) */
|
|
494
|
+
injectSseEvent?: {
|
|
495
|
+
/** Event type (e.g., "unknown", "control", "data") */
|
|
496
|
+
eventType: string;
|
|
497
|
+
/** Event data (will be sent as-is) */
|
|
498
|
+
data: string;
|
|
499
|
+
};
|
|
493
500
|
}
|
|
494
501
|
declare class DurableStreamTestServer {
|
|
495
502
|
readonly store: StreamStore | FileBackedStreamStore;
|
package/dist/index.js
CHANGED
|
@@ -1346,10 +1346,14 @@ const CURSOR_QUERY_PARAM = `cursor`;
|
|
|
1346
1346
|
* Line terminators in the payload (CR, LF, or CRLF) become separate data: lines.
|
|
1347
1347
|
* This prevents CRLF injection attacks where malicious payloads could inject
|
|
1348
1348
|
* fake SSE events using CR-only line terminators.
|
|
1349
|
+
*
|
|
1350
|
+
* Note: We don't add a space after "data:" because clients strip exactly one
|
|
1351
|
+
* leading space per the SSE spec. Adding one would cause data starting with
|
|
1352
|
+
* spaces to lose an extra space character.
|
|
1349
1353
|
*/
|
|
1350
1354
|
function encodeSSEData(payload) {
|
|
1351
1355
|
const lines = payload.split(/\r\n|\r|\n/);
|
|
1352
|
-
return lines.map((line) => `data
|
|
1356
|
+
return lines.map((line) => `data:${line}`).join(`\n`) + `\n\n`;
|
|
1353
1357
|
}
|
|
1354
1358
|
/**
|
|
1355
1359
|
* Minimum response size to consider for compression.
|
|
@@ -1531,10 +1535,12 @@ var DurableStreamTestServer = class {
|
|
|
1531
1535
|
if (fault.truncateBodyBytes !== void 0 && modified.length > fault.truncateBodyBytes) modified = modified.slice(0, fault.truncateBodyBytes);
|
|
1532
1536
|
if (fault.corruptBody && modified.length > 0) {
|
|
1533
1537
|
modified = new Uint8Array(modified);
|
|
1534
|
-
|
|
1538
|
+
modified[0] = 88;
|
|
1539
|
+
if (modified.length > 1) modified[1] = 89;
|
|
1540
|
+
const numCorrupt = Math.max(1, Math.floor(modified.length * .1));
|
|
1535
1541
|
for (let i = 0; i < numCorrupt; i++) {
|
|
1536
1542
|
const pos = Math.floor(Math.random() * modified.length);
|
|
1537
|
-
modified[pos] =
|
|
1543
|
+
modified[pos] = 90;
|
|
1538
1544
|
}
|
|
1539
1545
|
}
|
|
1540
1546
|
return modified;
|
|
@@ -1572,7 +1578,7 @@ var DurableStreamTestServer = class {
|
|
|
1572
1578
|
res.end(`Injected error for testing`);
|
|
1573
1579
|
return;
|
|
1574
1580
|
}
|
|
1575
|
-
if (fault.truncateBodyBytes !== void 0 || fault.corruptBody) res._injectedFault = fault;
|
|
1581
|
+
if (fault.truncateBodyBytes !== void 0 || fault.corruptBody || fault.injectSseEvent) res._injectedFault = fault;
|
|
1576
1582
|
}
|
|
1577
1583
|
try {
|
|
1578
1584
|
switch (method) {
|
|
@@ -1813,6 +1819,11 @@ var DurableStreamTestServer = class {
|
|
|
1813
1819
|
"x-content-type-options": `nosniff`,
|
|
1814
1820
|
"cross-origin-resource-policy": `cross-origin`
|
|
1815
1821
|
});
|
|
1822
|
+
const fault = res._injectedFault;
|
|
1823
|
+
if (fault?.injectSseEvent) {
|
|
1824
|
+
res.write(`event: ${fault.injectSseEvent.eventType}\n`);
|
|
1825
|
+
res.write(`data: ${fault.injectSseEvent.data}\n\n`);
|
|
1826
|
+
}
|
|
1816
1827
|
let currentOffset = initialOffset;
|
|
1817
1828
|
let isConnected = true;
|
|
1818
1829
|
const decoder = new TextDecoder();
|
|
@@ -2007,10 +2018,10 @@ var DurableStreamTestServer = class {
|
|
|
2007
2018
|
res.end(`Missing required field: path`);
|
|
2008
2019
|
return;
|
|
2009
2020
|
}
|
|
2010
|
-
const hasFaultType = config.status !== void 0 || config.delayMs !== void 0 || config.dropConnection || config.truncateBodyBytes !== void 0 || config.corruptBody;
|
|
2021
|
+
const hasFaultType = config.status !== void 0 || config.delayMs !== void 0 || config.dropConnection || config.truncateBodyBytes !== void 0 || config.corruptBody || config.injectSseEvent !== void 0;
|
|
2011
2022
|
if (!hasFaultType) {
|
|
2012
2023
|
res.writeHead(400, { "content-type": `text/plain` });
|
|
2013
|
-
res.end(`Must specify at least one fault type: status, delayMs, dropConnection, truncateBodyBytes, or
|
|
2024
|
+
res.end(`Must specify at least one fault type: status, delayMs, dropConnection, truncateBodyBytes, corruptBody, or injectSseEvent`);
|
|
2014
2025
|
return;
|
|
2015
2026
|
}
|
|
2016
2027
|
this.injectFault(config.path, {
|
|
@@ -2023,7 +2034,8 @@ var DurableStreamTestServer = class {
|
|
|
2023
2034
|
probability: config.probability,
|
|
2024
2035
|
method: config.method,
|
|
2025
2036
|
corruptBody: config.corruptBody,
|
|
2026
|
-
jitterMs: config.jitterMs
|
|
2037
|
+
jitterMs: config.jitterMs,
|
|
2038
|
+
injectSseEvent: config.injectSseEvent
|
|
2027
2039
|
});
|
|
2028
2040
|
res.writeHead(200, { "content-type": `application/json` });
|
|
2029
2041
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -2105,13 +2117,13 @@ function createRegistryHooks(store, serverUrl) {
|
|
|
2105
2117
|
createdAt: event.timestamp
|
|
2106
2118
|
}
|
|
2107
2119
|
});
|
|
2108
|
-
await registryStream.append(changeEvent);
|
|
2120
|
+
await registryStream.append(JSON.stringify(changeEvent));
|
|
2109
2121
|
},
|
|
2110
2122
|
onStreamDeleted: async (event) => {
|
|
2111
2123
|
await ensureRegistryExists();
|
|
2112
2124
|
const streamName = extractStreamName(event.path);
|
|
2113
2125
|
const changeEvent = registryStateSchema.streams.delete({ key: streamName });
|
|
2114
|
-
await registryStream.append(changeEvent);
|
|
2126
|
+
await registryStream.append(JSON.stringify(changeEvent));
|
|
2115
2127
|
}
|
|
2116
2128
|
};
|
|
2117
2129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@durable-streams/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Node.js reference server implementation for Durable Streams",
|
|
5
5
|
"author": "Durable Stream contributors",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -39,15 +39,15 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@neophi/sieve-cache": "^1.0.0",
|
|
41
41
|
"lmdb": "^3.3.0",
|
|
42
|
-
"@durable-streams/client": "0.
|
|
43
|
-
"@durable-streams/state": "0.
|
|
42
|
+
"@durable-streams/client": "0.2.0",
|
|
43
|
+
"@durable-streams/state": "0.2.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.0.0",
|
|
47
47
|
"tsdown": "^0.9.0",
|
|
48
48
|
"typescript": "^5.0.0",
|
|
49
49
|
"vitest": "^4.0.0",
|
|
50
|
-
"@durable-streams/server-conformance-tests": "0.
|
|
50
|
+
"@durable-streams/server-conformance-tests": "0.2.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"dist",
|
package/src/registry-hook.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function createRegistryHooks(
|
|
|
100
100
|
},
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
await registryStream.append(changeEvent)
|
|
103
|
+
await registryStream.append(JSON.stringify(changeEvent))
|
|
104
104
|
},
|
|
105
105
|
|
|
106
106
|
onStreamDeleted: async (event) => {
|
|
@@ -112,7 +112,7 @@ export function createRegistryHooks(
|
|
|
112
112
|
key: streamName,
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
await registryStream.append(changeEvent)
|
|
115
|
+
await registryStream.append(JSON.stringify(changeEvent))
|
|
116
116
|
},
|
|
117
117
|
}
|
|
118
118
|
}
|
package/src/server.ts
CHANGED
|
@@ -42,12 +42,16 @@ const CURSOR_QUERY_PARAM = `cursor`
|
|
|
42
42
|
* Line terminators in the payload (CR, LF, or CRLF) become separate data: lines.
|
|
43
43
|
* This prevents CRLF injection attacks where malicious payloads could inject
|
|
44
44
|
* fake SSE events using CR-only line terminators.
|
|
45
|
+
*
|
|
46
|
+
* Note: We don't add a space after "data:" because clients strip exactly one
|
|
47
|
+
* leading space per the SSE spec. Adding one would cause data starting with
|
|
48
|
+
* spaces to lose an extra space character.
|
|
45
49
|
*/
|
|
46
50
|
function encodeSSEData(payload: string): string {
|
|
47
51
|
// Split on all SSE-valid line terminators: CRLF, CR, or LF
|
|
48
52
|
// Order matters: \r\n must be matched before \r alone
|
|
49
53
|
const lines = payload.split(/\r\n|\r|\n/)
|
|
50
|
-
return lines.map((line) => `data
|
|
54
|
+
return lines.map((line) => `data:${line}`).join(`\n`) + `\n\n`
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
/**
|
|
@@ -127,6 +131,13 @@ interface InjectedFault {
|
|
|
127
131
|
corruptBody?: boolean
|
|
128
132
|
/** Add jitter to delay (random 0-jitterMs added to delayMs) */
|
|
129
133
|
jitterMs?: number
|
|
134
|
+
/** Inject an SSE event with custom type and data (for testing SSE parsing) */
|
|
135
|
+
injectSseEvent?: {
|
|
136
|
+
/** Event type (e.g., "unknown", "control", "data") */
|
|
137
|
+
eventType: string
|
|
138
|
+
/** Event data (will be sent as-is) */
|
|
139
|
+
data: string
|
|
140
|
+
}
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
export class DurableStreamTestServer {
|
|
@@ -371,14 +382,20 @@ export class DurableStreamTestServer {
|
|
|
371
382
|
modified = modified.slice(0, fault.truncateBodyBytes)
|
|
372
383
|
}
|
|
373
384
|
|
|
374
|
-
// Corrupt body if configured
|
|
385
|
+
// Corrupt body if configured - deterministically break JSON structure
|
|
375
386
|
if (fault.corruptBody && modified.length > 0) {
|
|
376
387
|
modified = new Uint8Array(modified) // Make a copy to avoid mutating original
|
|
377
|
-
//
|
|
378
|
-
|
|
388
|
+
// Always corrupt the first byte (breaks JSON structure - the opening [ or {)
|
|
389
|
+
// and add some random corruption for good measure
|
|
390
|
+
modified[0] = 0x58 // 'X' - makes JSON syntactically invalid
|
|
391
|
+
if (modified.length > 1) {
|
|
392
|
+
modified[1] = 0x59 // 'Y'
|
|
393
|
+
}
|
|
394
|
+
// Also corrupt some bytes in the middle to catch edge cases
|
|
395
|
+
const numCorrupt = Math.max(1, Math.floor(modified.length * 0.1))
|
|
379
396
|
for (let i = 0; i < numCorrupt; i++) {
|
|
380
397
|
const pos = Math.floor(Math.random() * modified.length)
|
|
381
|
-
modified[pos] =
|
|
398
|
+
modified[pos] = 0x5a // 'Z' - valid UTF-8 but breaks JSON structure
|
|
382
399
|
}
|
|
383
400
|
}
|
|
384
401
|
|
|
@@ -454,8 +471,12 @@ export class DurableStreamTestServer {
|
|
|
454
471
|
return
|
|
455
472
|
}
|
|
456
473
|
|
|
457
|
-
// Store fault for response modification (truncation, corruption)
|
|
458
|
-
if (
|
|
474
|
+
// Store fault for response modification (truncation, corruption, SSE injection)
|
|
475
|
+
if (
|
|
476
|
+
fault.truncateBodyBytes !== undefined ||
|
|
477
|
+
fault.corruptBody ||
|
|
478
|
+
fault.injectSseEvent
|
|
479
|
+
) {
|
|
459
480
|
;(
|
|
460
481
|
res as ServerResponse & { _injectedFault?: InjectedFault }
|
|
461
482
|
)._injectedFault = fault
|
|
@@ -868,6 +889,15 @@ export class DurableStreamTestServer {
|
|
|
868
889
|
"cross-origin-resource-policy": `cross-origin`,
|
|
869
890
|
})
|
|
870
891
|
|
|
892
|
+
// Check for injected SSE event (for testing SSE parsing)
|
|
893
|
+
const fault = (res as ServerResponse & { _injectedFault?: InjectedFault })
|
|
894
|
+
._injectedFault
|
|
895
|
+
if (fault?.injectSseEvent) {
|
|
896
|
+
// Send the injected SSE event before normal stream
|
|
897
|
+
res.write(`event: ${fault.injectSseEvent.eventType}\n`)
|
|
898
|
+
res.write(`data: ${fault.injectSseEvent.data}\n\n`)
|
|
899
|
+
}
|
|
900
|
+
|
|
871
901
|
let currentOffset = initialOffset
|
|
872
902
|
let isConnected = true
|
|
873
903
|
const decoder = new TextDecoder()
|
|
@@ -1208,6 +1238,11 @@ export class DurableStreamTestServer {
|
|
|
1208
1238
|
method?: string
|
|
1209
1239
|
corruptBody?: boolean
|
|
1210
1240
|
jitterMs?: number
|
|
1241
|
+
// SSE event injection (for testing SSE parsing)
|
|
1242
|
+
injectSseEvent?: {
|
|
1243
|
+
eventType: string
|
|
1244
|
+
data: string
|
|
1245
|
+
}
|
|
1211
1246
|
}
|
|
1212
1247
|
|
|
1213
1248
|
if (!config.path) {
|
|
@@ -1222,11 +1257,12 @@ export class DurableStreamTestServer {
|
|
|
1222
1257
|
config.delayMs !== undefined ||
|
|
1223
1258
|
config.dropConnection ||
|
|
1224
1259
|
config.truncateBodyBytes !== undefined ||
|
|
1225
|
-
config.corruptBody
|
|
1260
|
+
config.corruptBody ||
|
|
1261
|
+
config.injectSseEvent !== undefined
|
|
1226
1262
|
if (!hasFaultType) {
|
|
1227
1263
|
res.writeHead(400, { "content-type": `text/plain` })
|
|
1228
1264
|
res.end(
|
|
1229
|
-
`Must specify at least one fault type: status, delayMs, dropConnection, truncateBodyBytes, or
|
|
1265
|
+
`Must specify at least one fault type: status, delayMs, dropConnection, truncateBodyBytes, corruptBody, or injectSseEvent`
|
|
1230
1266
|
)
|
|
1231
1267
|
return
|
|
1232
1268
|
}
|
|
@@ -1242,6 +1278,7 @@ export class DurableStreamTestServer {
|
|
|
1242
1278
|
method: config.method,
|
|
1243
1279
|
corruptBody: config.corruptBody,
|
|
1244
1280
|
jitterMs: config.jitterMs,
|
|
1281
|
+
injectSseEvent: config.injectSseEvent,
|
|
1245
1282
|
})
|
|
1246
1283
|
|
|
1247
1284
|
res.writeHead(200, { "content-type": `application/json` })
|