Skip to content

处理不同类型的请求体

不同的客户端发送不同格式的数据,服务器需要正确解析。

常见请求体类型

Content-Type描述示例
application/jsonJSON 数据{"name":"John"}
application/x-www-form-urlencoded表单数据name=John&age=30
multipart/form-data文件上传二进制数据
text/plain纯文本Hello World

解析 JSON

最常见的 API 数据格式:

javascript
const http = require('http');

async function readBody(req) {
  const chunks = [];
  for await (const chunk of req) {
    chunks.push(chunk);
  }
  return Buffer.concat(chunks).toString();
}

async function parseJSON(req) {
  const body = await readBody(req);
  if (!body) return null;
  return JSON.parse(body);
}

const server = http.createServer(async (req, res) => {
  if (req.method === 'POST' && req.headers['content-type'] === 'application/json') {
    try {
      const data = await parseJSON(req);
      console.log('收到 JSON:', data);
      
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ received: data }));
    } catch (err) {
      res.statusCode = 400;
      res.end('Invalid JSON');
    }
  } else {
    res.statusCode = 404;
    res.end('Not Found');
  }
});

server.listen(3000);

解析 URL 编码表单

HTML 表单默认使用这种格式:

javascript
function parseFormUrlEncoded(body) {
  const params = new URLSearchParams(body);
  return Object.fromEntries(params);
}

const server = http.createServer(async (req, res) => {
  const contentType = req.headers['content-type'] || '';
  
  if (req.method === 'POST' && contentType.includes('application/x-www-form-urlencoded')) {
    const body = await readBody(req);
    const data = parseFormUrlEncoded(body);
    
    console.log('表单数据:', data);
    // { name: 'John', email: 'john@example.com' }
    
    res.end('表单已接收');
  } else {
    res.end('OK');
  }
});

统一解析器

根据 Content-Type 自动选择解析方式:

javascript
async function parseBody(req) {
  const contentType = req.headers['content-type'] || '';
  const body = await readBody(req);
  
  if (!body) return null;
  
  if (contentType.includes('application/json')) {
    return JSON.parse(body);
  }
  
  if (contentType.includes('application/x-www-form-urlencoded')) {
    return Object.fromEntries(new URLSearchParams(body));
  }
  
  // 其他类型返回原始字符串
  return body;
}

const server = http.createServer(async (req, res) => {
  if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
    try {
      const data = await parseBody(req);
      console.log('请求体:', data);
      
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ success: true, data }));
    } catch (err) {
      res.statusCode = 400;
      res.end(JSON.stringify({ error: 'Parse error' }));
    }
  } else {
    res.end('OK');
  }
});

处理大请求体

防止恶意大请求耗尽内存:

javascript
async function readBodyWithLimit(req, maxSize = 1024 * 1024) {
  const chunks = [];
  let size = 0;
  
  for await (const chunk of req) {
    size += chunk.length;
    
    if (size > maxSize) {
      throw new Error('Request body too large');
    }
    
    chunks.push(chunk);
  }
  
  return Buffer.concat(chunks).toString();
}

const server = http.createServer(async (req, res) => {
  if (req.method === 'POST') {
    try {
      const body = await readBodyWithLimit(req, 1024 * 100); // 100KB 限制
      const data = JSON.parse(body);
      
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({ received: true }));
    } catch (err) {
      if (err.message === 'Request body too large') {
        res.statusCode = 413;
        res.end('Payload Too Large');
      } else {
        res.statusCode = 400;
        res.end('Bad Request');
      }
    }
  } else {
    res.end('OK');
  }
});

检查 Content-Length

javascript
const server = http.createServer(async (req, res) => {
  const contentLength = parseInt(req.headers['content-length'] || '0', 10);
  const maxSize = 1024 * 1024; // 1MB
  
  // 预检大小
  if (contentLength > maxSize) {
    res.statusCode = 413;
    res.end('Payload Too Large');
    return;
  }
  
  // 处理请求...
  const body = await readBody(req);
  res.end('OK');
});

处理文件上传

multipart/form-data 解析较复杂,通常使用第三方库。这里展示基本原理:

javascript
// 简化示例,实际应用推荐使用 formidable 或 busboy
function parseMultipart(body, boundary) {
  const parts = body.split(`--${boundary}`);
  const result = {};
  
  for (const part of parts) {
    if (part.includes('Content-Disposition')) {
      const nameMatch = part.match(/name="([^"]+)"/);
      if (nameMatch) {
        const name = nameMatch[1];
        const content = part.split('\r\n\r\n')[1]?.trim();
        result[name] = content;
      }
    }
  }
  
  return result;
}

const server = http.createServer(async (req, res) => {
  const contentType = req.headers['content-type'] || '';
  
  if (contentType.includes('multipart/form-data')) {
    const boundary = contentType.split('boundary=')[1];
    const body = await readBody(req);
    const data = parseMultipart(body, boundary);
    
    console.log('上传数据:', data);
    res.end('Upload received');
  } else {
    res.end('OK');
  }
});

完整的请求体解析器

javascript
const http = require('http');

class BodyParser {
  constructor(options = {}) {
    this.maxSize = options.maxSize || 1024 * 1024;
  }
  
  async parse(req) {
    const contentType = req.headers['content-type'] || '';
    
    // 检查大小限制
    const contentLength = parseInt(req.headers['content-length'] || '0', 10);
    if (contentLength > this.maxSize) {
      const error = new Error('Payload too large');
      error.status = 413;
      throw error;
    }
    
    // 读取请求体
    const body = await this.read(req);
    
    // 根据类型解析
    if (contentType.includes('application/json')) {
      return this.parseJSON(body);
    }
    
    if (contentType.includes('application/x-www-form-urlencoded')) {
      return this.parseForm(body);
    }
    
    return body;
  }
  
  async read(req) {
    const chunks = [];
    let size = 0;
    
    for await (const chunk of req) {
      size += chunk.length;
      if (size > this.maxSize) {
        const error = new Error('Payload too large');
        error.status = 413;
        throw error;
      }
      chunks.push(chunk);
    }
    
    return Buffer.concat(chunks).toString();
  }
  
  parseJSON(body) {
    if (!body) return null;
    try {
      return JSON.parse(body);
    } catch {
      const error = new Error('Invalid JSON');
      error.status = 400;
      throw error;
    }
  }
  
  parseForm(body) {
    if (!body) return {};
    return Object.fromEntries(new URLSearchParams(body));
  }
}

// 使用
const parser = new BodyParser({ maxSize: 100 * 1024 });

const server = http.createServer(async (req, res) => {
  try {
    if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
      req.body = await parser.parse(req);
    }
    
    // 处理业务逻辑
    console.log('请求体:', req.body);
    
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ success: true, body: req.body }));
    
  } catch (err) {
    res.statusCode = err.status || 500;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ error: err.message }));
  }
});

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

测试请求

bash
# JSON
curl -X POST http://localhost:3000 \
  -H "Content-Type: application/json" \
  -d '{"name":"John","age":30}'

# 表单
curl -X POST http://localhost:3000 \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "name=John&age=30"

本章小结

  • 使用 for await...of 读取请求体流
  • 根据 Content-Type 选择解析方式
  • JSON 使用 JSON.parse()
  • 表单使用 URLSearchParams
  • 设置大小限制防止内存耗尽
  • 文件上传推荐使用专门的库

下一章我们将学习路由的基础实现。

处理不同类型的请求体 has loaded