MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js HTTP 请求中的错误捕获与响应

2024-01-305.6k 阅读

Node.js HTTP 请求基础

在深入探讨错误捕获与响应之前,我们先来回顾一下 Node.js 中 HTTP 请求的基本概念。Node.js 提供了内置的 http 模块,它允许我们创建 HTTP 服务器和发起 HTTP 请求。

创建 HTTP 服务器

通过 http 模块创建一个简单的 HTTP 服务器非常容易。以下是一个基本示例:

const http = require('http');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});

const port = 3000;
server.listen(port, () => {
  console.log(`Server running at port ${port}`);
});

在上述代码中,我们使用 http.createServer 方法创建了一个服务器实例。该方法接受一个回调函数,该回调函数在每次接收到 HTTP 请求时被调用。回调函数的两个参数 reqhttp.IncomingMessage 类型)和 reshttp.ServerResponse 类型)分别代表请求和响应对象。

发起 HTTP 请求

发起 HTTP 请求在 Node.js 中同样简单。http 模块提供了 http.request 方法来实现这一功能。以下是一个发起 GET 请求的示例:

const http = require('http');

const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET'
};

const req = http.request(options, (res) => {
  let data = '';

  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    console.log('Response received:', data);
  });
});

req.end();

在这个例子中,我们使用 http.request 方法创建了一个请求对象 reqhttp.request 方法的第一个参数是一个包含请求选项的对象,例如主机名、端口、路径和请求方法等。第二个参数是一个回调函数,该回调函数在接收到响应时被调用。

错误类型及产生场景

在 HTTP 请求过程中,可能会出现多种类型的错误。了解这些错误类型及其产生场景对于有效的错误捕获和处理至关重要。

网络相关错误

  1. 连接超时:当请求在规定的时间内未能成功建立连接时,会触发连接超时错误。这可能是由于目标服务器负载过高、网络延迟过大或者目标服务器不可达等原因导致的。
  2. 网络中断:在请求过程中,如果网络连接突然中断,例如网线被拔掉或者无线连接断开,就会产生网络中断错误。

服务器相关错误

  1. 4xx 客户端错误:这类错误表示客户端发送的请求存在问题,例如 400 Bad Request 表示请求语法错误,401 Unauthorized 表示未经授权,404 Not Found 表示请求的资源不存在等。
  2. 5xx 服务器错误:这类错误表示服务器在处理请求时发生了错误,例如 500 Internal Server Error 表示服务器内部错误,503 Service Unavailable 表示服务器暂时不可用等。

请求配置错误

  1. 错误的 URL:如果在发起请求时设置了错误的 URL,例如主机名拼写错误或者路径错误,就会导致请求失败。
  2. 不支持的请求方法:如果使用了目标服务器不支持的请求方法,例如对不支持 PUT 方法的服务器发起 PUT 请求,就会引发错误。

错误捕获

使用 try...catch 捕获同步错误

在 Node.js 中,虽然大多数与 HTTP 请求相关的操作是异步的,但在某些情况下,例如请求配置过程中,可能会出现同步错误。对于这类错误,可以使用传统的 try...catch 语句进行捕获。

const http = require('http');

try {
  const options = {
    hostname: 'www.example.com',
    port: 'not a number', // 故意设置错误的端口值
    path: '/',
    method: 'GET'
  };

  const req = http.request(options, (res) => {
    // 处理响应
  });

  req.end();
} catch (error) {
  console.error('同步错误捕获:', error.message);
}

在上述代码中,由于我们将 port 设置为了非数字值,在调用 http.request 时会抛出一个同步错误,这个错误会被 try...catch 语句捕获。

监听事件捕获异步错误

对于异步操作中的错误,例如网络连接错误或者服务器响应错误,我们需要通过监听特定的事件来捕获错误。

捕获请求错误

在发起 HTTP 请求时,可以通过监听 req 对象的 error 事件来捕获请求过程中的错误。

const http = require('http');

const options = {
  hostname: 'nonexistent.example.com', // 故意设置不存在的主机名
  port: 80,
  path: '/',
  method: 'GET'
};

const req = http.request(options, (res) => {
  // 处理响应
});

req.on('error', (error) => {
  console.error('请求错误捕获:', error.message);
});

req.end();

在这个例子中,由于我们设置了一个不存在的主机名,在尝试建立连接时会触发 error 事件,我们通过监听该事件捕获到了这个错误。

捕获响应错误

在接收到 HTTP 响应后,我们可以通过检查响应状态码来判断是否发生了错误。对于 4xx 和 5xx 状态码,我们可以认为是发生了错误。

const http = require('http');

const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/nonexistent', // 故意设置不存在的路径
  method: 'GET'
};

const req = http.request(options, (res) => {
  if (res.statusCode >= 400) {
    console.error('响应错误捕获:', `状态码 ${res.statusCode}`);
  }

  let data = '';
  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    console.log('响应内容:', data);
  });
});

req.end();

在上述代码中,我们故意请求了一个不存在的路径,服务器会返回一个 404 状态码。我们通过检查 res.statusCode 判断是否发生了错误,并进行相应的处理。

错误响应

向客户端发送错误响应

当在服务器端捕获到错误时,需要向客户端发送合适的错误响应,以便客户端能够了解发生了什么问题。

简单文本错误响应

const http = require('http');

const server = http.createServer((req, res) => {
  try {
    // 模拟一些可能出错的逻辑
    throw new Error('服务器内部错误');
  } catch (error) {
    res.statusCode = 500;
    res.setHeader('Content-Type', 'text/plain');
    res.end('服务器内部错误');
  }
});

const port = 3000;
server.listen(port, () => {
  console.log(`Server running at port ${port}`);
});

在这个例子中,当服务器内部发生错误时,我们设置状态码为 500,并向客户端发送简单的文本错误信息。

JSON 格式错误响应

在现代 Web 开发中,JSON 格式的响应更为常见,因为它易于解析和处理。

const http = require('http');

const server = http.createServer((req, res) => {
  try {
    // 模拟一些可能出错的逻辑
    throw new Error('请求参数错误');
  } catch (error) {
    res.statusCode = 400;
    res.setHeader('Content-Type', 'application/json');
    const errorResponse = {
      error: '请求参数错误',
      message: error.message
    };
    res.end(JSON.stringify(errorResponse));
  }
});

const port = 3000;
server.listen(port, () => {
  console.log(`Server running at port ${port}`);
});

在上述代码中,我们将错误信息以 JSON 格式发送给客户端,客户端可以更方便地解析和处理这些错误信息。

处理客户端错误响应

当客户端发起 HTTP 请求并接收到错误响应时,需要根据错误类型进行相应的处理。

处理 404 错误

const http = require('http');

const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/nonexistent',
  method: 'GET'
};

const req = http.request(options, (res) => {
  if (res.statusCode === 404) {
    console.log('资源未找到,可能需要提示用户检查路径');
  }

  let data = '';
  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    console.log('响应内容:', data);
  });
});

req.end();

在这个例子中,当接收到 404 状态码时,我们在控制台输出提示信息,告知可能需要检查路径。在实际应用中,可以根据具体需求进行更复杂的处理,例如引导用户到正确的页面。

处理 500 错误

const http = require('http');

const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET'
};

const req = http.request(options, (res) => {
  if (res.statusCode === 500) {
    console.log('服务器内部错误,可能需要重试或联系管理员');
  }

  let data = '';
  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    console.log('响应内容:', data);
  });
});

req.end();

当接收到 500 状态码时,我们在控制台输出提示信息,告知用户可能需要重试或联系管理员。同样,在实际应用中,可以根据业务需求进行更合适的处理,例如自动重试一定次数或者显示友好的错误页面。

高级错误处理策略

错误日志记录

在生产环境中,记录错误日志对于排查问题非常重要。可以使用 Node.js 内置的 console 模块或者第三方日志库,如 winston 来记录错误日志。

使用 console 记录错误日志

const http = require('http');

const server = http.createServer((req, res) => {
  try {
    // 模拟一些可能出错的逻辑
    throw new Error('服务器内部错误');
  } catch (error) {
    console.error('服务器错误:', error.message, error.stack);
    res.statusCode = 500;
    res.setHeader('Content-Type', 'text/plain');
    res.end('服务器内部错误');
  }
});

const port = 3000;
server.listen(port, () => {
  console.log(`Server running at port ${port}`);
});

在上述代码中,我们使用 console.error 记录了错误信息和堆栈跟踪,这对于定位错误发生的位置非常有帮助。

使用 winston 记录错误日志

首先需要安装 winston

npm install winston

然后可以使用以下代码记录错误日志:

const http = require('http');
const winston = require('winston');

const logger = winston.createLogger({
  level: 'error',
  format: winston.format.json(),
  transports: [
    new winston.transport.Console(),
    new winston.transport.File({ filename: 'error.log' })
  ]
});

const server = http.createServer((req, res) => {
  try {
    // 模拟一些可能出错的逻辑
    throw new Error('服务器内部错误');
  } catch (error) {
    logger.error({
      message: error.message,
      stack: error.stack
    });
    res.statusCode = 500;
    res.setHeader('Content-Type', 'text/plain');
    res.end('服务器内部错误');
  }
});

const port = 3000;
server.listen(port, () => {
  console.log(`Server running at port ${port}`);
});

在这个例子中,我们使用 winston 创建了一个日志记录器,它将错误信息以 JSON 格式记录到控制台和 error.log 文件中。

错误重试机制

对于一些由于临时网络故障或者服务器负载过高导致的错误,可以考虑实现错误重试机制。

const http = require('http');
const { promisify } = require('util');

const requestWithRetry = async (options, maxRetries = 3, retryDelay = 1000) => {
  let retries = 0;
  while (retries < maxRetries) {
    try {
      const req = http.request(options, (res) => {
        // 处理响应
      });

      const sendRequest = promisify(req.end.bind(req));
      await sendRequest();
      return;
    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw error;
      }
      console.log(`重试 ${retries} 次,错误信息:`, error.message);
      await new Promise((resolve) => setTimeout(resolve, retryDelay));
    }
  }
};

const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET'
};

requestWithRetry(options).catch((error) => {
  console.error('最终请求失败:', error.message);
});

在上述代码中,我们定义了一个 requestWithRetry 函数,它会在请求失败时进行重试,最多重试 3 次,每次重试间隔 1 秒。如果最终仍然失败,则抛出错误。

全局错误处理

在大型 Node.js 应用中,为了统一管理错误,可以实现全局错误处理机制。

const http = require('http');

process.on('uncaughtException', (error) => {
  console.error('全局未捕获异常:', error.message, error.stack);
  // 可以在这里进行一些额外的处理,例如发送错误通知到监控系统
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('全局未处理拒绝:', reason, 'Promise:', promise);
  // 同样可以进行额外的处理,例如记录日志或者发送通知
});

const server = http.createServer((req, res) => {
  // 模拟一些可能出错的异步操作
  setTimeout(() => {
    throw new Error('异步操作错误');
  }, 1000);
});

const port = 3000;
server.listen(port, () => {
  console.log(`Server running at port ${port}`);
});

在这个例子中,我们通过监听 process 对象的 uncaughtExceptionunhandledRejection 事件来捕获全局未捕获的异常和未处理的 Promise 拒绝。在实际应用中,可以在这些事件处理函数中进行更复杂的操作,例如记录详细的错误日志、发送错误通知到监控系统等。

通过以上全面的错误捕获与响应策略,我们可以构建更加健壮和可靠的 Node.js HTTP 应用。无论是在开发小型项目还是大型企业级应用时,这些策略都能有效地提高应用的稳定性和用户体验。在实际应用中,需要根据具体的业务需求和场景进行适当的调整和优化,以达到最佳的错误处理效果。同时,持续关注 Node.js 生态系统中关于错误处理的新特性和最佳实践,也能帮助我们不断提升应用的质量和性能。