@lzpong/httpm 1.2.0 → 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 +20 -2
  2. package/httpm.js +58 -58
  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 实例 |
@@ -224,6 +225,7 @@ app.ws('/chat/:room', (ws, req) => {
224
225
  ```javascript
225
226
  app.wss.broadcast('/chat', 'Hello everyone'); // 按路径分组广播
226
227
  app.wss.broadcast('/chat', 'Hello', ws); // 排除指定连接(传 ws 对象)
228
+ app.wss.broadcastAll('Hello everyone'); // 全局广播
227
229
  app.wss.getConnections(); // 获取所有连接
228
230
  ```
229
231
 
@@ -237,6 +239,13 @@ app.wss.getConnections(); // 获取所有连接
237
239
  | `close` | 连接关闭,回调参数 `(code, reason)` |
238
240
  | `error` | 连接错误 |
239
241
 
242
+ ### WebSocket 方法
243
+
244
+ | 方法 | 说明 |
245
+ |------|------|
246
+ | `ws.send(data)` | 发送消息(自动区分文本/JSON/二进制) |
247
+ | `ws.close(code, reason)` | 关闭连接,可选状态码和原因 |
248
+
240
249
  ### data 事件使用
241
250
 
242
251
  ```javascript
@@ -382,6 +391,15 @@ const app = httpm({
382
391
  });
383
392
  ```
384
393
 
394
+ ## 关闭服务
395
+
396
+ ```javascript
397
+ // 关闭 HTTP/HTTPS 服务器并断开所有 WebSocket 连接
398
+ app.close(() => {
399
+ console.log('Server closed');
400
+ });
401
+ ```
402
+
385
403
  ## 导出接口
386
404
 
387
405
  ```javascript
package/httpm.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * httpm - 基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库
3
3
  *
4
4
  * @name httpm
5
- * @version 1.2.0
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);
@@ -2191,6 +2185,8 @@ class Application extends Router {
2191
2185
  * 错误处理:查找错误处理中间件(4 个参数)
2192
2186
  */
2193
2187
  _handleError(err, req, res, stack, startIdx) {
2188
+ // 无错误时直接返回(错误处理中间件调用 next() 无参数表示错误已处理)
2189
+ if (!err) return;
2194
2190
  // 从当前栈中查找错误处理中间件
2195
2191
  for (let i = startIdx; i < stack.length; i++) {
2196
2192
  const handler = stack[i].handler;
@@ -2347,35 +2343,37 @@ class Application extends Router {
2347
2343
  return;
2348
2344
  }
2349
2345
 
2350
- const items = entries.map(entry => {
2351
- try {
2352
- // 目录只需名称和类型,文件需要额外 stat 获取大小和修改时间
2353
- if (entry.isDirectory()) {
2354
- return { name: entry.name, isDirectory: true, size: 0, modified: '' };
2355
- }
2356
- const stat = fs.statSync(path.join(dirPath, entry.name));
2357
- 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 => ({
2358
2353
  name: entry.name,
2359
2354
  isDirectory: false,
2360
2355
  size: stat.size,
2361
2356
  modified: stat.mtime.toISOString()
2362
- };
2363
- } catch (e) {
2364
- return null;
2365
- }
2366
- }).filter(Boolean);
2367
-
2368
- // 排序:目录在前
2369
- items.sort((a, b) => {
2370
- if (a.isDirectory && !b.isDirectory) return -1;
2371
- if (!a.isDirectory && b.isDirectory) return 1;
2372
- return a.name.localeCompare(b.name);
2357
+ }))
2358
+ .catch(() => null);
2373
2359
  });
2374
2360
 
2375
- // 生成 HTML 目录列表
2376
- const html = this._renderDirectoryHTML(requestPath, items);
2377
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
2378
- 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
+ });
2379
2377
  });
2380
2378
  }
2381
2379
 
@@ -2384,23 +2382,20 @@ class Application extends Router {
2384
2382
  */
2385
2383
  _renderDirectoryHTML(requestPath, items) {
2386
2384
  // requestPath 已去掉前导 /,空字符串表示根目录
2387
- // path.dirname('subdir') 返回 '.',应映射为根目录 ''
2388
- let parentPath = requestPath ? path.dirname(requestPath) : '/';
2389
- if (parentPath === '.') parentPath = '';
2390
2385
  // 根目录时显示 '/',否则显示请求路径
2391
2386
  const displayPath = requestPath || '/';
2392
2387
  let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
2393
2388
  html += `<style>body{font-family:-apple-system,sans-serif;margin:20px;background:#f5f5f5}h1{font-size:18px;color:#333}table{width:100%;border-collapse:collapse;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.1)}th{text-align:left;padding:10px 12px;background:#f8f8f8;border-bottom:2px solid #ddd;font-size:13px;color:#666}td{padding:8px 12px;border-bottom:1px solid #eee;font-size:13px}a{color:#0066cc;text-decoration:none}a:hover{text-decoration:underline}.dir{font-weight:bold}.size{color:#999}</style>`;
2394
2389
  html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
2395
2390
 
2396
- // 父目录链接(根目录时不显示)
2391
+ // 父目录链接:使用相对路径 .. 返回上一级(根目录时不显示)
2397
2392
  if (requestPath && requestPath !== '/') {
2398
- html += `<tr><td><a href="${escapeHtml(parentPath)}" class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
2393
+ html += `<tr><td><a href=".." class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
2399
2394
  }
2400
2395
 
2401
2396
  for (const item of items) {
2402
- // 使用 '/' 拼接路径,防止 Windows 上 path.join 产生反斜杠导致 href 失效
2403
- const href = requestPath ? requestPath + '/' + item.name : item.name;
2397
+ // 仅使用文件名作为相对 href,页面 URL 本身已包含目录路径
2398
+ const href = item.name;
2404
2399
  const name = item.isDirectory ? item.name + '/' : item.name;
2405
2400
  const size = item.isDirectory ? '-' : fmtSize(item.size);
2406
2401
  const cls = item.isDirectory ? 'dir' : '';
@@ -2415,6 +2410,11 @@ class Application extends Router {
2415
2410
  * 处理 WebSocket 升级请求
2416
2411
  */
2417
2412
  _handleUpgrade(req, socket, head) {
2413
+ // 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
2414
+ if (!this._wss) {
2415
+ socket.destroy();
2416
+ return;
2417
+ }
2418
2418
  const ws = this._wss.handleUpgrade(req, socket, head);
2419
2419
  if (!ws) return;
2420
2420
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzpong/httpm",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库,兼容 Express API",
5
5
  "main": "httpm.js",
6
6
  "files": [