返回

一次 egg-static Range 请求头报错的处理之旅

前端

一次 egg-static Range 请求头报错的处理之旅

背景

由于团队里有用到 eggjs 做后端服务,之前一直用 egg-static 作为图片的静态资源服务,用户上传图片到 eggjs 的静态资源目录后,就可以通过 url 访问,非常方便。最近接到一个新需求,需要上传视频文件到静态资源目录,同时还要求支持视频的 Range 请求,以便实现视频的断点续传功能。

问题

在实现 Range 请求支持后,我发现当用户请求视频文件时,会出现以下错误:

Error: 416 Requested Range Not Satisfiable

经过一番排查,我发现问题出在 egg-static 对 Range 请求头的处理上。在 egg-static 中,Range 请求头的处理逻辑如下:

if (range && range.type === 'bytes') {
  const start = range.start;
  const end = range.end;
  const fileLength = fs.statSync(file).size;
  if (start > fileLength || end > fileLength) {
    return ctx.status = 416;
  }
  ctx.set('Content-Range', `bytes ${start}-${end}/${fileLength}`);
  ctx.set('Content-Length', end - start + 1);
  return fs.createReadStream(file, { start, end }).pipe(ctx.res);
}

这段代码首先检查 Range 请求头中的 start 和 end 参数是否大于文件长度,如果大于则返回 416 状态码。然后设置 Content-Range 和 Content-Length 头,最后通过 fs.createReadStream 创建一个可读流,并将该流管道到 ctx.res。

解决方案

经过分析,我发现问题的原因是当用户请求的 Range 超出文件长度时,egg-static 并没有正确处理。根据 HTTP 规范,当 Range 请求超出文件长度时,服务器应该返回 206 Partial Content 状态码,而不是 416 Requested Range Not Satisfiable。

为了解决这个问题,我修改了 egg-static 的 Range 请求头处理逻辑,如下:

if (range && range.type === 'bytes') {
  const start = range.start;
  const end = range.end;
  const fileLength = fs.statSync(file).size;
  if (start > fileLength || end > fileLength) {
    return ctx.status = 206;
    ctx.set('Content-Range', `bytes */${fileLength}`);
  }
  ctx.set('Content-Range', `bytes ${start}-${end}/${fileLength}`);
  ctx.set('Content-Length', end - start + 1);
  return fs.createReadStream(file, { start, end }).pipe(ctx.res);
}

修改后的代码首先检查 Range 请求头中的 start 和 end 参数是否大于文件长度,如果大于则返回 206 状态码,并设置 Content-Range 头为 bytes */${fileLength}。然后设置 Content-Length 头,最后通过 fs.createReadStream 创建一个可读流,并将该流管道到 ctx.res。

这样,当我再次请求视频文件时,就不再出现 416 错误了。

总结

通过这次报错的处理,我学到了很多东西。首先,我了解了 egg-static 对 Range 请求头的处理逻辑。其次,我了解了 HTTP 规范中对 Range 请求头的规定。最后,我学会了如何修改 egg-static 的源代码来满足我的需求。

我相信,这次经历对我的成长很有帮助。