Hi gays, 你造Content-Disposition吗?

9,840 阅读5分钟

背景

某个工作日的下午,一个前端小伙伴找我说:“我的线上的页面原来是好好的,今天访问的时候怎么变成了下载了,我什么都没做呀?”正在这时,又有人反馈所有的web网页访问的时候都变成下载了。

经过紧急的排查,发现问题的原因是阿里云某个 CDN 节点的回源请求的构建出现了问题,导致 OSS 源站无法识别 CDN 请求,并对其插入 content-disposition头,最终影响了浏览器对文件的处理逻辑。(本应该浏览器渲染的,被当做附件下载)。

所以今天要和大家聊的就是导致这起线上问题的 Content-disposition 到底是何方神圣。

Content-Disposition

Content-Disposition 有两种应用场景。

用在HTTP响应头中

场景一是用在HTTP的响应头中,指示响应的内容该以何种形式展示。是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

Content-Disposition 的第一个参数的值有两个:

  • inline 默认值,表示响应中的消息体会以页面的一部分或者整个页面的形式展示。
  • attachment 表示响应的消息体应被下载到本地;大多数浏览器会出现一个“保存为”的对话框。

第二个参数是可选的 filename。当响应的内第一个参数指定为 attachment 时,浏览器会将响应中的内容下载下来,filename 可以指定下载后的文件名。

Content-Disposition 在响应头中可能会这样出现:

// 正常解析渲染
Content-Disposition: inline
// 下载文件
Content-Disposition: attachment
// 下载文件,并将文件保存为filename.jpg
Content-Disposition: attachment; filename="filename.jpg"

用在 multipart/form-data的请求体中

还有一种场景是,当页面上有表单,并且我们选择的表单提交方式为 multipart/form-data 时,Content-Disposition 会出现在请求体中。其作用是说明对应的表单项的字段名是什么,表单中上传的文件名是什么。在该场景下第一个参数总是固定不变的 form-data,另外有两个可选参数。name 表示对应的表单项的字段名,filename 表示对应的上传的文件名。参数之间使用 ; 进行分割。

Content-Dispositionmultipart/form-data 的请求体头中可能会这样出现:

Content-Disposition: form-data
Content-Disposition: form-data; name="fieldName"
Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"

示例

接下来,我们通过一个示例来加深一下对它的印象。该示例主要演示两部分功能。

  • 通过设置 Content-Disposition: attachment 在浏览器中下载保存请求的文件。
  • 通过实现一个 post 请求,来说明 Content-Disposition: form-data 在请求体中的展示形式。

创建 contentDispositionDemo 目录,执行:

npm install express multiparty

服务端代码我们使用 express来实现,同时使用 multiparty 模块来处理 multipart/form-data 的请求。 服务端代码如下:

// app.js
const express = require("express");
const multiparty = require("multiparty");
const app = express();
const port = 3000;
app.use(express.static("public"));

// 访问的时候,弹窗提示下载attachment.html,并保存为ddd.html
app.get("/attach", (req, res, next) => {
  const options = {
    root: __dirname,
    headers: {
      "Content-Disposition": "attachment;filename=ddd.html"
    }
  };
  res.sendFile("/attachment.html", options, err => {
    if (err) {
      next(err);
    } else {
      console.log("Sent:", "attachment.html");
    }
  });
});

// form-data表单提交
app.post("/user", (req, res, next) => {
  const form = new multiparty.Form();
  var name;
  var image;
  form.on("error", next);
  form.on("close", function(err) {
    console.log(err);
    console.log(name);
    console.log(image);
    res.send("资料提交成功!");
  });

  form.on("field", function(key, val) {
    if (key !== "image") name = val;
  });

  form.on("part", function(part) {
    if (!part.filename) return;
    if (part.name !== "image") return part.resume();
    image = {};
    image.filename = part.filename;
    image.size = 0;
    part.on("data", function(buf) {
      image.size += buf.length;
    });
  });

  form.parse(req);
});

app.listen(port, () => console.log(`App listening on port ${port}!`));

接下来我们创建一个简单的表单提交页面,用户需要填写姓名和一张照片。我们将该页面放在 public 目录下,页面代码如下:

<!--public/index.html-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>content-disposition 示例</title>
    <style>
      form {
        padding: 20px;
        border: 1px gray solid;
        background: yellowgreen;
      }
      .form-item {
        padding: 10px 0;
      }
      #commit {
        display: block;
        width: 80px;
        height: 30px;
        text-align: center;
        line-height: 30px;
        border: 1px solid #666;
        cursor: pointer;
        background: #eee;
      }
    </style>
  </head>
  <body>
    <h1>请提交您的个人资料</h1>
    <form>
      <div class="form-item">
        <label for="name">姓名:</label>
        <input type="text" name="name" id="name" />
      </div>
      <div class="form-item">
        <label for="pic">照片:</label>
        <input type="file" name="image" id="image" />
      </div>
      <div class="form-item"><span id="commit">提交</span></div>
    </form>
  </body>
  <script>
    function upload() {
      var formData = new FormData();
      var name = document.querySelector("#name").value;
      var image = document.querySelector("#image").files[0];
      formData.append("name", name);
      formData.append("image", image);

      var xhr = new XMLHttpRequest();
      xhr.open("POST", "/user");
      xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
          console.log(xhr.responseText);
          alert(xhr.responseText);
        }
      };
      xhr.send(formData);
    }

    document.querySelector("#commit").addEventListener(
      "click",
      function() {
        upload();
      },
      false
    );
  </script>
</html>

另外,再创建一个HTML页面,该页面用于浏览器访问的时候下载。具体代码内容随意:

<!--attachment.html-->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>保存文件</title>
  </head>
  <body>
    保存文件
  </body>
</html>

执行 node app.js 启动服务,当我们访问 localhost:3000/attach的时候,可以发现浏览器直接下载了 attachment.html,并将其保存为了 ddd.html

以下为页面下载的截图:

响以下为响应头的截图:

访问localhost:3000,在首页的表单中输入信息并提交,然后我们看到下图中 Content-Disposition 的展示形式:

总结

由于当我们使用 Content-Disposition 默认值为 inline的时候,在请求的响应头中是不显示的,所以我猜很多前端的小伙伴可能对 Content-Disposition 了解不够。其主要用途还是用来告知浏览器如何处理响应中的内容。当然,出现文章开篇的事故是个意外。通过该文章对其有了较为详细的了解后,相信前端小伙伴们都可以熟练应用它来达到不同的目的了。

关于我们

快狗打车前端团队专注前端技术分享,定期推送高质量文章,欢迎关注点赞。
文章同步发布在公众号哟,想要第一时间得到最新的资讯,just scan it !

公众号二维码