@lzpong/httpm 1.2.1 → 1.2.2

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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/httpm.js +52 -51
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -139,8 +139,9 @@ app.use((err, req, res, next) => {
139
139
  | `res.redirect([code,] url)` | 重定向,兼容 Express 签名:`redirect(url)` 默认 302,`redirect(status, url)` 指定状态码 |
140
140
  | `res.cookie(name, value, opts)` | 设置 Cookie |
141
141
  | `res.clearCookie(name, opts)` | 清除 Cookie |
142
- | `res.set(name, value)` / `res.setHeader()` | 设置响应头(set 支持对象批量) |
143
- | `res.get(name)` / `res.getHeader()` | 获取响应头 |
142
+ | `res.set(name, value)` / `res.setHeader()` | 设置响应头(set 支持对象批量;setHeader 为底层方法,功能相同) |
143
+ | `res.get(name)` / `res.getHeader()` | 获取响应头(get 为 Express 兼容方法,功能相同) |
144
+ | `res.removeHeader(name)` | 移除已设置的响应头 |
144
145
  | `res.type(type)` | 设置 Content-Type(支持简写:html→text/html) |
145
146
  | `res.on(event, fn)` | 监听响应事件(finish/close 等) |
146
147
  | `res.sse()` | 创建 SSE 实例 |
package/httpm.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * httpm - 基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库
3
3
  *
4
4
  * @name httpm
5
- * @version 1.2.1
5
+ * @version 1.2.2
6
6
  * @description 兼容 Express API,内置路由、中间件、静态文件服务、
7
7
  * WebSocket、SSE、流式上传、日志系统等功能
8
8
  * @license MIT
@@ -1045,8 +1045,6 @@ class WebSocket {
1045
1045
  this._handlers = {};
1046
1046
  // 最大帧负载大小(默认 100MB,防止恶意超大帧耗尽内存)
1047
1047
  this._maxPayload = options.maxPayload || 100 * 1024 * 1024;
1048
- // 写入背压标记
1049
- this._writeBacklogged = false;
1050
1048
  // 帧解析状态:缓存不完整帧数据
1051
1049
  this._frameBuffer = Buffer.alloc(0);
1052
1050
  // 分片帧状态
@@ -1090,7 +1088,7 @@ class WebSocket {
1090
1088
  while (this._frameBuffer.length >= 2) {
1091
1089
  const result = this._decodeFrame(this._frameBuffer);
1092
1090
  if (result === null) break; // 数据不完整,等待更多数据
1093
- this._frameBuffer = this._frameBuffer.slice(result.bytesConsumed);
1091
+ this._frameBuffer = this._frameBuffer.subarray(result.bytesConsumed);
1094
1092
  this._processFrame(result.opcode, result.payload, result.fin, result.oversize);
1095
1093
  }
1096
1094
  }
@@ -1129,7 +1127,7 @@ class WebSocket {
1129
1127
  let mask = null;
1130
1128
  if (isMasked) {
1131
1129
  if (buf.length < offset + 4) return null;
1132
- mask = buf.slice(offset, offset + 4);
1130
+ mask = buf.subarray(offset, offset + 4);
1133
1131
  offset += 4;
1134
1132
  }
1135
1133
 
@@ -1142,7 +1140,7 @@ class WebSocket {
1142
1140
  }
1143
1141
 
1144
1142
  // 提取负载
1145
- let payload = buf.slice(offset, offset + payloadLength);
1143
+ let payload = buf.subarray(offset, offset + payloadLength);
1146
1144
  if (isMasked && mask) {
1147
1145
  for (let i = 0; i < payload.length; i++) {
1148
1146
  payload[i] ^= mask[i % 4];
@@ -1174,7 +1172,7 @@ class WebSocket {
1174
1172
  code = payload.readUInt16BE(0);
1175
1173
  // RFC 6455 Section 7.4: 状态码 0-999 为非法,1005 表示无状态码
1176
1174
  if (code < 1000) code = 1005;
1177
- reason = payload.length > 2 ? payload.slice(2).toString('utf8') : '';
1175
+ reason = payload.length > 2 ? payload.subarray(2).toString('utf8') : '';
1178
1176
  }
1179
1177
  // 如果正在关闭握手中,对端已回复 Close 帧,完成握手
1180
1178
  if (this._closing) {
@@ -1282,11 +1280,7 @@ class WebSocket {
1282
1280
  frames.push(payload);
1283
1281
  const frame = Buffer.concat(frames);
1284
1282
  try {
1285
- const canWrite = this.socket.write(frame);
1286
- // 写入缓冲区满时记录警告(WebSocket 不像 HTTP 可暂停请求流,只能记录)
1287
- if (!canWrite) {
1288
- this._writeBacklogged = true;
1289
- }
1283
+ this.socket.write(frame);
1290
1284
  } catch (e) {
1291
1285
  this.connected = false;
1292
1286
  // 发送失败时触发 error 事件,便于用户感知和处理
@@ -1705,14 +1699,14 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1705
1699
  if (idx === -1) {
1706
1700
  // 保留尾部回看字节,防止分隔符跨 chunk 截断
1707
1701
  if (buffer.length > lookBehind) {
1708
- buffer = buffer.slice(buffer.length - lookBehind);
1702
+ buffer = buffer.subarray(buffer.length - lookBehind);
1709
1703
  }
1710
1704
  break;
1711
1705
  }
1712
- buffer = buffer.slice(idx + delimiter.length);
1706
+ buffer = buffer.subarray(idx + delimiter.length);
1713
1707
  // 跳过 \r\n
1714
1708
  if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
1715
- buffer = buffer.slice(2);
1709
+ buffer = buffer.subarray(2);
1716
1710
  }
1717
1711
  state = 'HEADERS';
1718
1712
  partHeadersBuf = Buffer.alloc(0);
@@ -1725,8 +1719,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1725
1719
  buffer = Buffer.alloc(0);
1726
1720
  break;
1727
1721
  }
1728
- partHeadersBuf = Buffer.concat([partHeadersBuf, buffer.slice(0, headerEnd)]);
1729
- buffer = buffer.slice(headerEnd + 4);
1722
+ partHeadersBuf = Buffer.concat([partHeadersBuf, buffer.subarray(0, headerEnd)]);
1723
+ buffer = buffer.subarray(headerEnd + 4);
1730
1724
  const partHeaders = partHeadersBuf.toString('utf8');
1731
1725
 
1732
1726
  // 解析 Content-Disposition
@@ -1761,7 +1755,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1761
1755
  // 保留尾部回看字节,防止分隔符跨 chunk 截断
1762
1756
  const safeLen = Math.max(0, buffer.length - lookBehind);
1763
1757
  const chunk = buffer.toString('utf8', 0, safeLen);
1764
- fieldSize += chunk.length;
1758
+ fieldSize += safeLen;
1765
1759
  if (fieldSize > maxFieldSize) {
1766
1760
  const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
1767
1761
  err.status = 413;
@@ -1769,12 +1763,12 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1769
1763
  return;
1770
1764
  }
1771
1765
  currentField.value += chunk;
1772
- buffer = buffer.slice(safeLen);
1766
+ buffer = buffer.subarray(safeLen);
1773
1767
  break;
1774
1768
  }
1775
1769
  // 字段结束
1776
1770
  const chunk = buffer.toString('utf8', 0, idx);
1777
- fieldSize += chunk.length;
1771
+ fieldSize += idx;
1778
1772
  if (fieldSize > maxFieldSize) {
1779
1773
  const err = new Error(`Field exceeds maximum size of ${fmtSize(maxFieldSize)}`, { cause: { actual: fieldSize, maxSize: maxFieldSize } });
1780
1774
  err.status = 413;
@@ -1787,10 +1781,10 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1787
1781
  currentField.value = currentField.value.slice(0, -2);
1788
1782
  }
1789
1783
  req.formData.fields[currentField.name] = currentField.value;
1790
- buffer = buffer.slice(idx + delimiter.length);
1784
+ buffer = buffer.subarray(idx + delimiter.length);
1791
1785
  // 跳过 \r\n
1792
1786
  if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
1793
- buffer = buffer.slice(2);
1787
+ buffer = buffer.subarray(2);
1794
1788
  }
1795
1789
  state = 'HEADERS';
1796
1790
  partHeadersBuf = Buffer.alloc(0);
@@ -1801,7 +1795,7 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1801
1795
  // 还没结束,写入临时文件
1802
1796
  // 保留尾部回看字节,防止分隔符跨 chunk 截断
1803
1797
  const safeLen = Math.max(0, buffer.length - lookBehind);
1804
- const writeData = buffer.slice(0, safeLen);
1798
+ const writeData = buffer.subarray(0, safeLen);
1805
1799
  if (!currentFile.stream) {
1806
1800
  currentFile.stream = fs.createWriteStream(currentFile.path);
1807
1801
  // 背压处理:写入流满时暂停请求读取,drain 后恢复
@@ -1827,14 +1821,14 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1827
1821
  next(err);
1828
1822
  return;
1829
1823
  }
1830
- buffer = buffer.slice(safeLen);
1824
+ buffer = buffer.subarray(safeLen);
1831
1825
  break;
1832
1826
  }
1833
1827
  // 文件结束
1834
- const fileData = buffer.slice(0, idx);
1828
+ const fileData = buffer.subarray(0, idx);
1835
1829
  // 去掉文件数据前的 \r\n
1836
1830
  const trimmedData = fileData.length >= 2 && fileData.at(-2) === 0x0D && fileData.at(-1) === 0x0A
1837
- ? fileData.slice(0, -2)
1831
+ ? fileData.subarray(0, -2)
1838
1832
  : fileData;
1839
1833
 
1840
1834
  if (!currentFile.stream) {
@@ -1862,10 +1856,10 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1862
1856
 
1863
1857
  // 清理上传进度条目
1864
1858
  cleanupOnError = false;
1865
- buffer = buffer.slice(idx + delimiter.length);
1859
+ buffer = buffer.subarray(idx + delimiter.length);
1866
1860
  // 跳过 \r\n
1867
1861
  if (buffer.length >= 2 && buffer[0] === 0x0D && buffer[1] === 0x0A) {
1868
- buffer = buffer.slice(2);
1862
+ buffer = buffer.subarray(2);
1869
1863
  }
1870
1864
  state = 'HEADERS';
1871
1865
  partHeadersBuf = Buffer.alloc(0);
@@ -2349,35 +2343,37 @@ class Application extends Router {
2349
2343
  return;
2350
2344
  }
2351
2345
 
2352
- const items = entries.map(entry => {
2353
- try {
2354
- // 目录只需名称和类型,文件需要额外 stat 获取大小和修改时间
2355
- if (entry.isDirectory()) {
2356
- return { name: entry.name, isDirectory: true, size: 0, modified: '' };
2357
- }
2358
- const stat = fs.statSync(path.join(dirPath, entry.name));
2359
- return {
2346
+ // 异步获取文件信息,避免同步阻塞事件循环
2347
+ const tasks = entries.map(entry => {
2348
+ if (entry.isDirectory()) {
2349
+ return Promise.resolve({ name: entry.name, isDirectory: true, size: 0, modified: '' });
2350
+ }
2351
+ return fs.promises.stat(path.join(dirPath, entry.name))
2352
+ .then(stat => ({
2360
2353
  name: entry.name,
2361
2354
  isDirectory: false,
2362
2355
  size: stat.size,
2363
2356
  modified: stat.mtime.toISOString()
2364
- };
2365
- } catch (e) {
2366
- return null;
2367
- }
2368
- }).filter(Boolean);
2369
-
2370
- // 排序:目录在前
2371
- items.sort((a, b) => {
2372
- if (a.isDirectory && !b.isDirectory) return -1;
2373
- if (!a.isDirectory && b.isDirectory) return 1;
2374
- return a.name.localeCompare(b.name);
2357
+ }))
2358
+ .catch(() => null);
2375
2359
  });
2376
2360
 
2377
- // 生成 HTML 目录列表
2378
- const html = this._renderDirectoryHTML(requestPath, items);
2379
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
2380
- res.send(html);
2361
+ Promise.all(tasks).then(results => {
2362
+ const items = results.filter(Boolean);
2363
+ // 排序:目录在前
2364
+ items.sort((a, b) => {
2365
+ if (a.isDirectory && !b.isDirectory) return -1;
2366
+ if (!a.isDirectory && b.isDirectory) return 1;
2367
+ return a.name.localeCompare(b.name);
2368
+ });
2369
+
2370
+ // 生成 HTML 目录列表
2371
+ const html = this._renderDirectoryHTML(requestPath, items);
2372
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
2373
+ res.send(html);
2374
+ }).catch(() => {
2375
+ res.status(500).send('Internal Server Error');
2376
+ });
2381
2377
  });
2382
2378
  }
2383
2379
 
@@ -2414,6 +2410,11 @@ class Application extends Router {
2414
2410
  * 处理 WebSocket 升级请求
2415
2411
  */
2416
2412
  _handleUpgrade(req, socket, head) {
2413
+ // 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
2414
+ if (!this._wss) {
2415
+ socket.destroy();
2416
+ return;
2417
+ }
2417
2418
  const ws = this._wss.handleUpgrade(req, socket, head);
2418
2419
  if (!ws) return;
2419
2420
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzpong/httpm",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库,兼容 Express API",
5
5
  "main": "httpm.js",
6
6
  "files": [