上传pdf到oss并且实现在浏览器中预览的流程和踩坑
之前做过一个需求:实现在浏览器中预览 pdf。虽然需求比较简单,不过有一些小细节可以展开讲。
本文按照实际链路,梳理从上传 PDF 到浏览器预览的流程,以及其中遇到的几个问题。
1. 上传 PDF
上传文件前,服务端需要确定 Bucket、Object Key、访问凭证和文件内容。
下面使用 OSS Node.js SDK 上传一份 PDF,并在上传请求中设置与浏览器预览有关的请求头:
OSS Node.js SDK 官方文档[1]
const OSS = require("ali-oss");
const path = require("path");
async function uploadPdf() {
const client = new OSS({
region: "oss-<region-id>",
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
authorizationV4: true,
bucket: "example-bucket",
});
const headers = {
"Content-Type": "application/pdf",
"Content-Disposition": "inline; filename=\"document.pdf\"",
"x-oss-object-acl": "private",
"x-oss-forbid-overwrite": "true",
};
const objectKey = "files/document.pdf";
const localFilePath = path.normalize("document.pdf");
const result = await client.put(objectKey, localFilePath, { headers });
console.log("文件上传完成", result);
}
uploadPdf().catch(console.error);
Content-Type: application/pdf 告诉客户端文件内容是 PDF。Content-Disposition: inline 表示这份内容适合在浏览器中展示,filename 则提供建议文件名。
PutObject 官方文档[2]列出了上传时对于 content-disposition 的指定的请求头:
指定Object的展示形式。取值如下:
• Content-Disposition:inline:直接预览文件内容。
• Content-Disposition:attachment:以原文件名的形式下载到浏览器指定路径。
• Content-Disposition:attachment; filename=“yourFileName”:以自定义文件名的形式下载到浏览器指定路径。yourFileName用于自定义下载后的文件名称,例如example.jpg。
在 PutObject 请求中设置的 Content-Type、Content-Disposition 等字段,会被 OSS 保存为 Object Metadata。以后读取 Object 时,相应值会成为 HTTP 响应头。不过Object Metadata 不是浏览器最终响应的唯一来源。
OSS Metadata 到 HTTP 响应头
2. 获取访问 URL
公共读 Bucket 中的 Object 可以通过公开 URL 访问。私有 Bucket 则要求请求携带有效签名,通常由服务端生成有时效的预签名 URL。
使用预签名 URL 下载(Node.js SDK)[3]
const OSS = require("ali-oss");
async function generateSignatureUrl(fileName) {
const client = new OSS({
endpoint: "https://files.example.com", // 可以指定自定义域名
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: "example-bucket",
region: "oss-<region-id>",
authorizationV4: true,
cname: true,
});
return client.signatureUrlV4(
"GET",
3600,
{ headers: {} },
fileName,
);
}
generateSignatureUrl("files/document.pdf")
.then((url) => console.log("Generated URL:", url))
.catch(console.error);
预签名 URL 解决的是私有 Object 的临时访问权限,不决定文件应该预览还是下载。浏览器最终怎样处理 PDF,仍取决于访问域名和最终响应头。
3. 访问域名如何改变最终响应
同一份 Object 可以通过 OSS 默认域名或已经绑定到 Bucket 的自定义域名访问。两类域名可能得到不同的最终响应。
3.1. OSS 默认域名可能增加强制下载头
阿里云文件预览排查文档[4]说明了 OSS 默认域名的强制下载规则。
为降低特定文件通过默认域名直接执行的安全风险,命中规则的请求会收到:
x-oss-force-download: true
Content-Disposition: attachment
是否命中强制下载规则,取决于 Bucket、账号、地域和文件类型等条件。OSS 默认域名并不是在所有情况下都会强制下载全部文件,具体范围应以阿里云文档中的规则为准。
如果 Object Metadata 原本保存的是:
Content-Disposition: inline
请求命中默认域名的强制下载策略后,浏览器最终可能收到:
x-oss-force-download: true
Content-Disposition: attachment
这意味着问题不一定出在上传代码。即使上传时保存了 inline,OSS 仍可能在形成最终响应时加入 attachment。
3.2. 自定义域名不会增加默认域名下载头
自定义域名需要先绑定到 Bucket,再通过 CNAME 指向 OSS。它不是简单地把预签名 URL 中的域名替换掉。
阿里云自定义域名文档[5]说明,通过自定义域名访问时,OSS 不会增加默认域名使用的强制下载响应头。
但自定义域名不会主动添加 Content-Disposition: inline。它只是不会触发默认域名的强制下载规则,最终响应仍取决于 Object Metadata、Content-Type 和浏览器支持情况。
4. 浏览器如何处理 PDF 响应
浏览器收到响应后,主要通过 Content-Type 和 Content-Disposition 判断内容是什么,以及应该怎样处理。
4.1. Content-Type 描述内容是什么
Content-Type[6] 描述响应内容的媒体类型:
Content-Type: application/pdf
浏览器据此知道响应内容是 PDF。如果该字段缺失或被设置为 application/octet-stream,浏览器只能将它识别为通用二进制内容,可能无法选择正确的展示方式。
4.2. Content-Disposition 描述怎样处理内容
Content-Disposition[7] 用于说明响应内容应该内联展示,还是作为附件下载。
| 取值或参数 | 示例 | 含义 |
|---|---|---|
inline |
Content-Disposition: inline |
内容可以在页面中展示 |
attachment |
Content-Disposition: attachment |
内容应该作为附件下载 |
filename |
attachment; filename="document.pdf" |
建议使用的 ASCII 文件名 |
filename* |
attachment; filename*=UTF-8''... |
使用编码表示非 ASCII 文件名 |
filename 和 filename* 是附加参数,不是与 inline、attachment 并列的处理方式。
下面两组响应的 Content-Type 相同,但处理语义不同:
Content-Type: application/pdf
Content-Disposition: inline
浏览器支持 PDF 时,通常会直接展示内容。
Content-Type: application/pdf
Content-Disposition: attachment
浏览器知道内容是 PDF,但仍会按照附件语义处理。
4.3. CORS
如果前端通过 fetch 读取不同源的 pdfUrl,浏览器会 CORS 校验:
项目发起跨域 fetch
→ 浏览器发送 Origin
→ OSS 根据 Bucket 的 CORS 规则返回响应头
→ 浏览器检查当前 Origin 是否被允许
→ 通过后 JavaScript 才能读取响应
OSS 的 CORS 规则通常需要关注:
| 配置 | 作用 |
|---|---|
| Allowed Origin | 允许哪些页面来源读取 Object |
| Allowed Methods | 允许 GET、HEAD 等哪些方法 |
| Allowed Headers | 允许请求携带哪些请求头 |
| Expose Headers | 允许 JavaScript 读取哪些响应头 |
配置入口是:进入阿里云 OSS 控制台的 Bucket 列表,选择目标 Bucket,再打开 数据安全 → 跨域设置,单击 创建规则。
具体参数见阿里云 OSS 跨域设置文档[8]。
5. 选择 PDF 预览方式
前端拿到 PDF URL 后,可以让 JavaScript 读取文件并生成 Blob URL,也可以直接让浏览器加载远程 URL。两种方式经过的链路不同。
5.1. 方式一:JavaScript 读取 PDF,再生成 Blob URL
如果远程 URL 返回 attachment,仍可以先读取文件,再用本地 Blob URL 打开 PDF:
const response = await fetch(pdfUrl);
if (!response.ok) {
throw new Error(`PDF 请求失败:${response.status}`);
}
const pdfBlob = await response.blob();
const blobUrl = URL.createObjectURL(pdfBlob);
const previewWindow = window.open(blobUrl, "_blank");
if (!previewWindow) {
URL.revokeObjectURL(blobUrl);
throw new Error("浏览器阻止了预览窗口");
}
// 预览页面不再使用该 URL 后,再调用 URL.revokeObjectURL(blobUrl)。
这个方案重新构造了一个本地资源,能够避开远程 attachment 对顶层导航的影响,但没有改变远程响应本身。
它还要求 fetch 通过 CORS 校验,并需要等待整份 PDF 被读取。对于大文件,这会增加内存占用,也失去了浏览器直接加载远程 PDF 时可能使用的分段请求能力。
5.1.1. 踩坑:保存文件名退化为 UUID
Blob URL 形如下面这样:
blob:https://example.com/<uuid>
它不会自动继承远程响应中的 Content-Disposition 和文件名。
在本项目使用的浏览器中,打开 Blob URL 后,地址栏、页签和 PDF 查看器使用 UUID。保存文件时,默认文件名同样退化为 UUID,而不是远程响应建议的文件名。
Blob URL 文件名退化为 UUID 示意图
左:Blob URL、页签与 PDF 查看器使用 UUID;右:“另存为”没有获得远程 PDF 的建议文件名。
如果项目需要提供指定文件名的下载操作,可以另外创建带 download 属性的链接:
const downloadLink = document.createElement("a");
downloadLink.href = blobUrl;
downloadLink.download = "document.pdf";
downloadLink.click();
具体默认文件名和扩展名仍可能因浏览器而异。这里记录的是项目实测结果,不将它扩大为所有浏览器的统一行为。
5.2. 方式二:浏览器直接加载远程 PDF
如果远程 URL 返回正确的 Content-Type 和 Content-Disposition,更合适的方式是让浏览器直接处理 PDF:
window.open(pdfUrl, "_blank");
这条路径由浏览器直接请求远程 PDF,业务 JavaScript 不读取响应体,因此不依靠 CORS 完成预览。
配置 CORS 也不会把 Content-Disposition: attachment 改成 inline。如果直接打开 URL 仍然下载,需要检查 Object Metadata、实际访问域名和最终响应头。
6. 总结
OSS PDF 从上传到浏览器处理流程
排查 PDF 无法预览时,可以按同一顺序检查:
1. 上传时是否保存了正确的 Content-Type 和 Content-Disposition。
2. 访问的是 OSS 默认域名还是已经绑定的自定义域名。
3. 浏览器实际收到的响应头是 inline 还是 attachment。
4. 前端是在直接打开 URL,还是通过 fetch 读取文件。
5. 如果使用 fetch,Bucket 的 CORS 规则是否允许当前页面来源。
参考链接
[1] OSS Node.js SDK 官方文档
https://help.aliyun.com/zh/oss/developer-reference/nodejs-sdk/
[2] PutObject 官方文档
https://help.aliyun.com/zh/oss/developer-reference/putobject
[3] 使用预签名 URL 下载(Node.js SDK)
https://help.aliyun.com/zh/oss/developer-reference/download-objects-using-a-presigned-url-generated-with-oss-sdk-for-node-js
[4] 阿里云文件预览排查文档
https://help.aliyun.com/zh/oss/how-to-ensure-an-object-is-previewed-when-you-access-the-object
[5] 阿里云自定义域名文档
https://help.aliyun.com/zh/oss/user-guide/access-buckets-via-custom-domain-names
[6] Content-Type
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Type
[7] Content-Disposition
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Content-Disposition
[8] 阿里云 OSS 跨域设置文档
https://help.aliyun.com/zh/oss/user-guide/configure-cross-origin-resource-sharing
夜雨聆风