@lzpong/httpm 1.2.2 → 1.2.3

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 +4 -2
  2. package/httpm.js +58 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -122,9 +122,11 @@ app.use((err, req, res, next) => {
122
122
  | `req.body` | 请求体(JSON/URL-encoded 自动解析) |
123
123
  | `req.formData` | 表单数据(multipart 解析后) |
124
124
  | `req.cookies` | Cookie 对象 |
125
+ | `req.signedCookies` | 签名 Cookie 对象(需配置 `cookieParserSecret`) |
125
126
  | `req.get(name)` | 获取请求头(不区分大小写) |
126
127
  | `req.headers` | 请求头对象 |
127
128
  | `req.ip` | 客户端 IP |
129
+ | `req.hostname` | 请求域名 |
128
130
  | `req.protocol` | 协议(http/https) |
129
131
 
130
132
  ## 响应对象 (res)
@@ -134,8 +136,8 @@ app.use((err, req, res, next) => {
134
136
  | `res.status(code)` | 设置状态码,链式调用 |
135
137
  | `res.json(obj)` | 发送 JSON 响应 |
136
138
  | `res.send(data)` | 发送响应(自动识别类型;`null`→`"null"`,`undefined`→空响应) |
137
- | `res.sendFile(path, opts)` | 发送文件 |
138
- | `res.download(path, name)` | 下载文件 |
139
+ | `res.sendFile(path, opts)` | 发送文件,opts 支持 `{ root, contentType }` |
140
+ | `res.download(path, [name], [opts])` | 下载文件,兼容 Express 签名:opts 传递给 sendFile |
139
141
  | `res.redirect([code,] url)` | 重定向,兼容 Express 签名:`redirect(url)` 默认 302,`redirect(status, url)` 指定状态码 |
140
142
  | `res.cookie(name, value, opts)` | 设置 Cookie |
141
143
  | `res.clearCookie(name, opts)` | 清除 Cookie |
package/httpm.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * httpm - 基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库
3
3
  *
4
4
  * @name httpm
5
- * @version 1.2.2
5
+ * @version 1.2.3
6
6
  * @description 兼容 Express API,内置路由、中间件、静态文件服务、
7
7
  * WebSocket、SSE、流式上传、日志系统等功能
8
8
  * @license MIT
@@ -164,8 +164,7 @@ const MIME_TYPES = {
164
164
  * 根据文件后缀匹配标准 MIME 类型
165
165
  */
166
166
  function getMimeType(ext) {
167
- if (!ext) return 'application/octet-stream';
168
- const lower = ext.toLowerCase();
167
+ const lower = ext?.toString().toLowerCase();
169
168
  return MIME_TYPES[lower] || 'application/octet-stream';
170
169
  }
171
170
 
@@ -773,7 +772,12 @@ class Response {
773
772
  }
774
773
 
775
774
  const mime = getMimeType(path.extname(fullPath));
776
- this.setHeader('Content-Type', mime);
775
+ // Content-Type 优先级:options.contentType > 已设置的 Content-Type > 自动检测
776
+ if (options.contentType) {
777
+ this.setHeader('Content-Type', options.contentType);
778
+ } else if (!this.getHeader('Content-Type')) {
779
+ this.setHeader('Content-Type', mime);
780
+ }
777
781
  this.setHeader('Accept-Ranges', 'bytes');
778
782
 
779
783
  // ETag 缓存校验
@@ -872,10 +876,19 @@ class Response {
872
876
  /**
873
877
  * 触发浏览器文件下载,支持大文件进度展示
874
878
  */
875
- download(filePath, filename) {
876
- const name = filename || path.basename(filePath);
879
+ download(filePath, filename, options) {
880
+ // Express 兼容签名:download(path), download(path, filename), download(path, filename, options), download(path, options)
881
+ let name = filename;
882
+ let opts = options;
883
+ if (typeof filename === 'object' && filename !== null) {
884
+ // download(path, options) 形式
885
+ opts = filename;
886
+ name = null;
887
+ }
888
+ name = name || path.basename(filePath);
889
+ opts = opts || {};
877
890
  this.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(name)}"`);
878
- this.sendFile(filePath);
891
+ this.sendFile(filePath, opts);
879
892
  }
880
893
 
881
894
  /**
@@ -1686,10 +1699,8 @@ function _parseMultipart(req, boundary, maxFileSize, maxFieldSize, next) {
1686
1699
  let cleanupOnError = false;
1687
1700
  let paused = false;
1688
1701
 
1689
- // 确保临时目录存在
1690
- if (!fs.existsSync(tempDir)) {
1691
- fs.mkdirSync(tempDir, { recursive: true });
1692
- }
1702
+ // 确保临时目录存在(recursive: true 时目录已存在不报错,无需 existsSync)
1703
+ fs.mkdirSync(tempDir, { recursive: true });
1693
1704
 
1694
1705
  function processBuffer() {
1695
1706
  while (buffer.length > 0) {
@@ -2312,18 +2323,20 @@ class Application extends Router {
2312
2323
  }
2313
2324
 
2314
2325
  if (stat.isDirectory()) {
2315
- // 目录:查找 index.html
2326
+ // 目录:查找 index.html(异步检查避免阻塞事件循环)
2316
2327
  const indexPath = path.join(fullPath, 'index.html');
2317
- if (fs.existsSync(indexPath)) {
2318
- res.sendFile(path.relative(rootPath, indexPath), { root: rootPath });
2319
- return;
2320
- }
2321
- // 展示目录列表
2322
- if (this.settings.showDir) {
2323
- this._serveDirectory(req, res, fullPath, requestPath);
2324
- } else {
2325
- res.status(404).json({ error: 'Not Found', status: 404 });
2326
- }
2328
+ fs.stat(indexPath, (idxErr) => {
2329
+ if (!idxErr) {
2330
+ res.sendFile(path.relative(rootPath, indexPath), { root: rootPath });
2331
+ return;
2332
+ }
2333
+ // 展示目录列表
2334
+ if (this.settings.showDir) {
2335
+ this._serveDirectory(req, res, fullPath, requestPath);
2336
+ } else {
2337
+ res.status(404).json({ error: 'Not Found', status: 404 });
2338
+ }
2339
+ });
2327
2340
  return;
2328
2341
  }
2329
2342
 
@@ -2384,18 +2397,21 @@ class Application extends Router {
2384
2397
  // requestPath 已去掉前导 /,空字符串表示根目录
2385
2398
  // 根目录时显示 '/',否则显示请求路径
2386
2399
  const displayPath = requestPath || '/';
2400
+ // 构建链接前缀:确保以 / 开头并以 / 结尾,用于生成绝对路径 href
2401
+ const prefix = '/' + (requestPath ? requestPath.replace(/\/+$/, '') + '/' : '');
2387
2402
  let html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Directory: ${escapeHtml(displayPath)}</title>`;
2388
2403
  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>`;
2389
2404
  html += `</head><body><h1>Directory: ${escapeHtml(displayPath)}</h1><table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>`;
2390
2405
 
2391
- // 父目录链接:使用相对路径 .. 返回上一级(根目录时不显示)
2406
+ // 父目录链接:使用绝对路径返回上一级(根目录时不显示)
2392
2407
  if (requestPath && requestPath !== '/') {
2393
- html += `<tr><td><a href=".." class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
2408
+ const parentHref = prefix + '..';
2409
+ html += `<tr><td><a href="${escapeHtml(parentHref)}" class="dir">../</a></td><td class="size">-</td><td>-</td></tr>`;
2394
2410
  }
2395
2411
 
2396
2412
  for (const item of items) {
2397
- // 仅使用文件名作为相对 href,页面 URL 本身已包含目录路径
2398
- const href = item.name;
2413
+ // 使用绝对路径 href,避免 URL 缺少尾部斜杠时解析错误
2414
+ const href = prefix + item.name;
2399
2415
  const name = item.isDirectory ? item.name + '/' : item.name;
2400
2416
  const size = item.isDirectory ? '-' : fmtSize(item.size);
2401
2417
  const cls = item.isDirectory ? 'dir' : '';
@@ -2412,6 +2428,13 @@ class Application extends Router {
2412
2428
  _handleUpgrade(req, socket, head) {
2413
2429
  // 防御性检查:_wss 在 listen() 中初始化,正常流程不会为 null
2414
2430
  if (!this._wss) {
2431
+ this._logger.warn('WebSocket server not initialized (call listen() first)');
2432
+ socket.destroy();
2433
+ return;
2434
+ }
2435
+ // 校验 Upgrade 头必须为 websocket
2436
+ if (req.headers['upgrade']?.toLowerCase() !== 'websocket') {
2437
+ this._logger.warn('Invalid upgrade header:', req.headers['upgrade'], 'expected: websocket');
2415
2438
  socket.destroy();
2416
2439
  return;
2417
2440
  }
@@ -2600,13 +2623,16 @@ function staticMiddleware(rootPath, options = {}) {
2600
2623
  return next();
2601
2624
  }
2602
2625
  if (stat.isDirectory()) {
2603
- // 目录:尝试 index.html
2626
+ // 目录:尝试 index.html(异步检查避免阻塞事件循环)
2604
2627
  const indexPath = path.join(fullPath, 'index.html');
2605
- if (fs.existsSync(indexPath)) {
2606
- res.sendFile(path.relative(root, indexPath), { root });
2607
- return;
2608
- }
2609
- return next();
2628
+ fs.stat(indexPath, (idxErr) => {
2629
+ if (!idxErr) {
2630
+ res.sendFile(path.relative(root, indexPath), { root });
2631
+ return;
2632
+ }
2633
+ return next();
2634
+ });
2635
+ return;
2610
2636
  }
2611
2637
  res.sendFile(requestPath, { root });
2612
2638
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lzpong/httpm",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "基于 Node.js 原生模块的单文件、零依赖 HTTP 服务库,兼容 Express API",
5
5
  "main": "httpm.js",
6
6
  "files": [