HTTP 100 Continue
信息型状态响应码表示目前为止一切正常,客户端应该继续请求,如果已完成请求则忽略。
为了让服务器检查请求的首部,客户端必须在发送请求实体前,在初始化请求中发送 Expect: 100-continue
首部并接收 100 Continue
响应状态码。
✅ 使用场景:
当客户端准备发送大请求体 (比如上传大文件),但不确定服务器是否愿意处理,就可以先只发请求头,等服务器同意后再发请求体。
✅ 使用时机:
- 客户端希望避免不必要地上传大数据(服务器可能会拒绝)
- 常用于:
PUT
或POST
请求,带有大量 body 数据
🧠 通信流程(前后端交互)
👉 步骤如下:
- 客户端先发送请求头(带
Expect: 100-continue
)
makefile
POST /upload HTTP/1.1
Host: example.com
Content-Length: 12345678
Expect: 100-continue
Content-Type: application/json
-
服务器返回:
100 Continue
:你可以继续发送 body417 Expectation Failed
:我不想处理你的请求(拒绝)
-
客户端再发送请求体(如文件数据)
💡 前端怎么用?
前端一般不会手动构造 HTTP 请求头,但可以通过一些高级配置方式实现:
在 JavaScript(Fetch API)中 不能直接控制 Expect,但在 Node.js、curl、Postman 等工具中可以。
💻 后端如何支持(以 Node.js / NestJS 为例)
后端需要支持 Expect 请求头 并手动判断是否返回 100 Continue
。
Node.js 示例:
javascript
const http = require('http');
const server = http.createServer((req, res) => {
if (req.headers['expect'] === '100-continue') {
// 条件判断通过,通知客户端继续上传
res.writeContinue();
req.on('data', chunk => { /* 处理上传数据 */ });
req.on('end', () => {
res.writeHead(200);
res.end('Upload complete');
});
} else {
// 正常请求逻辑
}
});
server.listen(3000);
✅ 场景总结
场景 | 是否推荐使用 100 Continue |
---|---|
⬆️ 上传大文件(>10MB) | ✅ 非常合适,节省资源 |
🚀 普通 JSON 请求 | ❌ 没必要,用 200 就够 |
⛔ 拦截非法用户或权限检查 | ✅ 如果你在 header 中校验权限 |
🚀 Tips:
- 在实际开发中,很多现代框架(如 Express/NestJS)默认不会对
100 Continue
做处理。 - 要使用它,需要较底层的 HTTP 处理,或特定中间件支持。
🧠 背后原理详解:
🔁 TCP 连接建立:
前端建立一个 TCP连接(就一次),然后:
🥇 第一步:发送请求头(带 Expect: 100-continue
)
makefile
http
POST /upload HTTP/1.1
Host: example.com
Content-Length: 12345678
Expect: 100-continue
Content-Type: application/json
✅ 请求体没有发出去 ,浏览器或 HTTP 客户端在"等服务器回应"
(也就是 "我可以上传吗?")
🥈 第二步:服务端判断是否允许上传
服务端返回:
vbnet
http
HTTP/1.1 100 Continue
✅ 这时客户端收到信号:"可以继续",才会发出请求体部分
🥉 第三步:客户端发送请求体(如文件数据)
json
json
CopyEdit
{
"file": "base64内容或者字节流......"
}
整个过程都是在同一个 TCP 连接里完成的!只是客户端"分两次 flush 数据流"。
📊 数据流结构(可视化)
css
[TCP连接建立]
↓
[客户端发送 请求头 + Expect]
↓
[服务端返回 100 Continue]
↓
[客户端再发 请求体]
↓
[服务端处理后返回 200/201/...]
📌 小结
问题 | 答案 |
---|---|
建立了几次连接? | ✅ 只建立 一次 TCP 连接 |
发了几次 HTTP 请求? | ✅ 只发了 一次请求(只是分段传输) |
请求体什么时候发? | ✅ 收到 100 Continue 之后才发送 |
哪些库自动处理这个过程? | curl 、Node.js 、axios 等高级 HTTP 客户端都支持(浏览器原生 Fetch 不暴露这么低层) |
NestJS 自定义监听 100 Continue 请求的中间件/拦截器 示例
🛠️ NestJS 自定义中间件:continue.middleware.ts
typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class ContinueMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
if (req.headers['expect'] === '100-continue') {
console.log('⚠️ Received Expect: 100-continue');
// 你可以在这里判断是否愿意接收请求体,比如鉴权、IP、限制类型等
const allowUpload = true;
if (allowUpload) {
// 告诉客户端可以继续上传 body
res.writeContinue(); // 等同于 res.writeHead(100)
} else {
// 拒绝继续上传
res.status(417).end('Expectation Failed');
return;
}
}
next();
}
}
🧩 在模块中启用中间件
在对应模块(如 AppModule
)中添加这个中间件:
typescript
ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ContinueMiddleware } from './continue.middleware';
import { UploadController } from './upload.controller';
@Module({
controllers: [UploadController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ContinueMiddleware)
.forRoutes('upload'); // 仅对 /upload 生效
}
}
📥 控制器处理请求体(upload.controller.ts
)
less
ts
import { Controller, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('upload')
export class UploadController {
@Post()
handleUpload(@Req() req: Request, @Res() res: Response) {
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
console.log('📦 Upload complete:', body.slice(0, 100));
res.status(200).send('Upload complete');
});
}
}
🧪 测试(用 curl
)
json
bash
curl -v -X POST http://localhost:3000/upload \
-H "Expect: 100-continue" \
-H "Content-Type: application/json" \
--data '{"large": "data block..."}'
你会看到类似:
kotlin
http
> Expect: 100-continue
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
📌 总结
功能 | 实现 |
---|---|
检查 Expect: 100-continue |
自定义中间件 |
允许/拒绝请求体上传 | res.writeContinue() or 417 |
处理请求体数据 | 控制器里监听 req.on('data') |
结合 multer
来处理大文件上传,也可以在 100 Continue
响应后再挂载 multer
📦 什么是 multer
?
multer
是一个 用于 Node.js(Express/NestJS)的中间件库 ,专门用来处理 multipart/form-data
格式 的请求,也就是:
✅ 前端表单中上传文件的场景
比如:
ini
html
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" />
<button type="submit">上传</button>
</form>
后台接收到的是带有文件的请求,普通 body-parser
解析不了,而 multer
可以自动把这些文件处理成可用对象,你就能像读取 JSON 那样方便操作上传的文件。
🌟 主要功能
功能 | 示例 |
---|---|
处理文件上传 | 图片、视频、音频、PDF 等 |
自动保存文件 | 保存到磁盘指定目录 |
限制文件大小 | 防止上传超大文件 |
限制上传类型 | 限制只允许图片等 MIME |
📦 在 NestJS 中如何使用 multer
NestJS 已经内置了 multer
的支持,我们只需要配合 @UseInterceptors()
和 FileInterceptor()
即可。
🌰 示例:上传单个文件
less
ts
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('upload')
export class UploadController {
@Post()
@UseInterceptors(FileInterceptor('avatar'))
upload(@UploadedFile() file: Express.Multer.File) {
console.log('收到文件:', file.originalname);
return { filename: file.filename };
}
}
🛠️ 存储配置(本地或内存)
python
ts
import { diskStorage } from 'multer';
@UseInterceptors(
FileInterceptor('avatar', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueName = Date.now() + '-' + file.originalname;
cb(null, uniqueName);
},
}),
}),
)
💡 常见上传字段类型
拦截器 | 场景 |
---|---|
FileInterceptor('field') |
单文件上传 |
FilesInterceptor('field') |
多文件(同一字段)上传 |
AnyFilesInterceptor() |
任意字段、任意数量文件 |
FileFieldsInterceptor([{ name: 'avatar' }, { name: 'cv' }]) |
多字段、多文件 |
🚧 与 100 Continue
配合
当你使用 Expect: 100-continue
控制上传开始时,可以先用你前面写的中间件确认是否允许上传,再交给 multer
来解析上传内容。
⚠️ 注意事项
- 上传路径必须存在(或使用
fs.mkdir
动态创建) - 如果你想存到云服务(如 S3),需要配合
multer-s3
- 上传接口要设好文件大小限制,防止攻击或误上传大文件
前端上传大文件到你刚才的 NestJS + S3 接口 的完整示例
✅ 方法一:用 axios
(推荐,支持进度条)
javascript
ts
import axios from 'axios';
const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post('http://localhost:3000/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Expect': '100-continue', // 添加这个头部
},
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`上传进度:${percent}%`);
},
});
console.log('✅ 上传成功:', response.data);
};
✅ 方法二:原生 fetch(不支持进度)
javascript
ts
const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('http://localhost:3000/upload', {
method: 'POST',
headers: {
'Expect': '100-continue',
},
body: formData,
});
const result = await response.json();
console.log('✅ 上传成功:', result);
};
🧪 HTML 示例:快速测试界面
你可以在一个 HTML 页面中写这样一个上传按钮来测试:
typescript
html
<input type="file" id="fileInput" />
<button onclick="upload()">上传</button>
<script>
async function upload() {
const file = document.getElementById('fileInput').files[0];
const formData = new FormData();
formData.append('file', file);
const res = await fetch('http://localhost:3000/upload', {
method: 'POST',
headers: {
'Expect': '100-continue',
},
body: formData,
});
const data = await res.json();
alert('上传成功:' + data.url);
}
</script>
🧠 温馨提示:
注意点 | 说明 |
---|---|
CORS | 确保后端设置了允许跨域上传(加 @Header('Access-Control-Allow-Origin', '*') ) |
大文件 | 上传大文件建议设置最大限制,避免卡住服务 |
文件类型 | 可在前端或 multer 设置白名单,例如只允许 .zip , .jpg 等 |
上传进度 | 建议用 axios 比 fetch 更方便控制上传进度 |
文件夹分类 | 后端可以根据文件类型或上传时间自动存储到 S3 子目录中 |
扩展阅读
🧩 你现在的上传(multer + S3)是什么?
这是 一次性上传,也叫"普通表单上传":
-
前端构造
FormData
,整个文件作为一个file
字段 -
后端(NestJS)接收整个请求后,用
multer
直接转存到 AWS S3 -
文件太大会:
- 客户端上传过程中容易失败
- 服务端可能超时或内存飙高
🔁 什么是断点上传 / 分片上传?
断点上传是指把一个大文件切成多个小块(chunk) ,每块单独上传,上传失败的块可以重传。
优点:
普通上传 | 分片上传 | |
---|---|---|
网络异常恢复 | ❌ 整个失败重传 | ✅ 支持断点续传 |
大文件支持 | 🚨 容易卡住 | ✅ 稳定 |
并发 | ❌ 一次一个请求 | ✅ 多块并发上传 |
S3 支持 | ✅(直接上传) | ✅(叫 Multipart Upload) |
🧪 举个例子:阿里 OSS / AWS S3 的分片上传流程
- 前端把文件分成 5MB 一块
- 调后端 API 生成 multipart uploadId
- 每个 chunk 上传时携带
uploadId
和partNumber
- 所有 chunk 上传完后,再调一个 API 合并文件
🧠 总结对比
上传方式 | 说明 | 是否支持断点 |
---|---|---|
你当前的 multer + S3 |
表单上传(一次性) | ❌ 不支持断点续传 |
分片上传(multipart upload) | 每块单独上传 | ✅ 支持断点和大文件稳定上传 |
前端直传 S3 | 前端自己上传,每块带签名 | ✅ 高性能但安全控制更复杂 |
支持断点续传的分片上传系统 🎬
✅ 功能目标:S3 分片上传系统
模块 | 功能 |
---|---|
🧠 后端(NestJS) | - 初始化 multipart upload(生成 UploadId ) - 获取每个分片的签名 URL - 完成合并 |
📦 前端(React/Vue/HTML) | - 分割大文件为 chunk - 并发上传每个 chunk - 上传失败可重试 - 上传进度可视化 |
☁️ 存储 | AWS S3,原生支持分片上传 |
🚧 搭建路线图(分 3 步)
第 1 步:NestJS 后端接口设计
接口 | 功能 |
---|---|
POST /upload/initiate |
创建 multipart upload,返回 uploadId |
GET /upload/signed-url |
获取分片上传用的签名 URL |
POST /upload/complete |
合并所有分片 |
👉 这部分我可以帮你全写好,包括 AWS SDK 的配置。
第 2 步:前端分片上传逻辑
步骤 | 内容 |
---|---|
1 | 选择文件,读取并分片(建议每块 5MB+) |
2 | 发请求拿到 uploadId |
3 | 循环每块,拿到带签名的 PUT URL,上传 |
4 | 所有 chunk 上传完后,发 complete 请求 |
5 | 显示上传成功链接 or 回调处理 |
第 3 步:进阶功能(可以选做)
- ✅ 进度条可视化
- 🔁 自动断点续传(失败部分重传)
- 🔒 设置上传限制(如文件类型、大小)
- 🗂️ 存储到指定子目录 / 用户私有 bucket
- 🌪️ 上传中断自动恢复(前端保存上传进度)
✅ 项目结构预览(NestJS)
css
css
src/
├── upload/
│ ├── upload.controller.ts ← 所有接口定义
│ ├── upload.service.ts ← 核心逻辑:生成签名、完成上传
│ └── dto/
│ └── complete-upload.dto.ts
└── main.ts
📦 安装依赖
bash
bash
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
🔧 第一步:配置 AWS S3
🔐 .env
ini
env
AWS_REGION=ap-southeast-1
AWS_ACCESS_KEY_ID=你的accessKey
AWS_SECRET_ACCESS_KEY=你的secret
AWS_BUCKET=my-upload-bucket
✨ 第二步:NestJS 后端实现
📁 upload/upload.service.ts
typescript
ts
import { Injectable } from '@nestjs/common';
import { S3Client, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class UploadService {
private s3: S3Client;
private bucket: string;
constructor(private configService: ConfigService) {
this.s3 = new S3Client({
region: configService.get('AWS_REGION'),
credentials: {
accessKeyId: configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: configService.get('AWS_SECRET_ACCESS_KEY'),
},
});
this.bucket = configService.get('AWS_BUCKET');
}
async initiateMultipartUpload(filename: string) {
const command = new CreateMultipartUploadCommand({
Bucket: this.bucket,
Key: filename,
});
const res = await this.s3.send(command);
return res.UploadId;
}
async generatePresignedUrl(filename: string, partNumber: number, uploadId: string) {
const command = new UploadPartCommand({
Bucket: this.bucket,
Key: filename,
PartNumber: partNumber,
UploadId: uploadId,
});
const url = await getSignedUrl(this.s3, command, { expiresIn: 3600 });
return url;
}
async completeUpload(filename: string, uploadId: string, parts: { PartNumber: number; ETag: string }[]) {
const command = new CompleteMultipartUploadCommand({
Bucket: this.bucket,
Key: filename,
UploadId: uploadId,
MultipartUpload: {
Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
},
});
const res = await this.s3.send(command);
return res;
}
}
🎯 upload/upload.controller.ts
less
ts
import { Controller, Post, Body, Get, Query } from '@nestjs/common';
import { UploadService } from './upload.service';
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('initiate')
async initiateUpload(@Body() body: { filename: string }) {
const uploadId = await this.uploadService.initiateMultipartUpload(body.filename);
return { uploadId };
}
@Get('signed-url')
async getSignedUrl(
@Query('filename') filename: string,
@Query('partNumber') partNumber: number,
@Query('uploadId') uploadId: string,
) {
const url = await this.uploadService.generatePresignedUrl(filename, partNumber, uploadId);
return { url };
}
@Post('complete')
async completeUpload(@Body() body: { filename: string; uploadId: string; parts: { ETag: string; PartNumber: number }[] }) {
const res = await this.uploadService.completeUpload(body.filename, body.uploadId, body.parts);
return { message: 'Upload complete', url: res.Location };
}
}
✨ upload.module.ts
python
ts
import { Module } from '@nestjs/common';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule],
providers: [UploadService],
controllers: [UploadController],
})
export class UploadModule {}
✨ app.module.ts
python
ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UploadModule } from './upload/upload.module';
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true }), UploadModule],
})
export class AppModule {}
🚀 接口使用流程图
-
POST /upload/initiate
→
{ filename }
→ 返回:
uploadId
-
GET /upload/signed-url?filename=xx&partNumber=1&uploadId=xxx
→ 返回:S3 签名上传链接
-
前端用
PUT
把 chunk 上传到这个链接(带上Content-Type
) -
所有块上传后
POST /upload/complete
→
{ filename, uploadId, parts: [{ PartNumber, ETag }] }
→ 返回合并结果和最终文件 URL
🧩 前端上传逻辑函数(React + Hooks)
- 分割文件为多个块
- 获取每个分片的签名 URL
- 上传分片
- 重试失败的分片
- 上传完成后调用合并接口
代码结构:
typescript
tsx
import React, { useState } from 'react';
const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片 5MB
const MAX_RETRIES = 3; // 最多重试次数
const useUploadFile = () => {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [uploadId, setUploadId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const initiateUpload = async (file: File) => {
try {
const res = await fetch('http://localhost:3000/upload/initiate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name }),
});
const data = await res.json();
setUploadId(data.uploadId);
} catch (err) {
console.error('Error initiating upload:', err);
setError('Failed to initiate upload.');
}
};
const getSignedUrl = async (partNumber: number, file: File) => {
try {
const res = await fetch(
`http://localhost:3000/upload/signed-url?filename=${file.name}&partNumber=${partNumber}&uploadId=${uploadId}`,
);
const data = await res.json();
return data.url;
} catch (err) {
console.error('Error getting signed URL:', err);
throw new Error('Failed to get signed URL.');
}
};
const uploadChunk = async (chunk: Blob, url: string, retries: number = 0): Promise<boolean> => {
try {
const res = await fetch(url, {
method: 'PUT',
body: chunk,
headers: { 'Content-Type': 'application/octet-stream' },
});
if (res.ok) return true;
else throw new Error('Failed to upload chunk.');
} catch (err) {
if (retries < MAX_RETRIES) {
return uploadChunk(chunk, url, retries + 1); // 重试
}
return false;
}
};
const completeUpload = async (file: File, parts: { PartNumber: number; ETag: string }[]) => {
try {
const res = await fetch('http://localhost:3000/upload/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
uploadId,
parts,
}),
});
const data = await res.json();
return data.url; // 返回 S3 文件的最终 URL
} catch (err) {
console.error('Error completing upload:', err);
setError('Failed to complete upload.');
}
};
const uploadFile = async (file: File) => {
setUploading(true);
setProgress(0);
setError(null);
try {
// 1. 初始化上传
await initiateUpload(file);
if (!uploadId) return;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const parts: { PartNumber: number; ETag: string }[] = [];
// 2. 上传每个分片
for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
const start = (partNumber - 1) * CHUNK_SIZE;
const end = Math.min(partNumber * CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
// 获取签名 URL
const signedUrl = await getSignedUrl(partNumber, file);
// 上传分片
const success = await uploadChunk(chunk, signedUrl);
if (!success) {
throw new Error(`Failed to upload part ${partNumber}`);
}
// 记录分片信息(ETag)
parts.push({ PartNumber: partNumber, ETag: 'dummyETag' }); // AWS S3 会自动生成 ETag,模拟一下
setProgress(Math.round((partNumber / totalChunks) * 100));
}
// 3. 完成上传
const fileUrl = await completeUpload(file, parts);
console.log('File uploaded successfully:', fileUrl);
setUploading(false);
} catch (err) {
setUploading(false);
setError(err.message || 'Unknown error occurred.');
}
};
return { uploadFile, uploading, progress, error };
};
const FileUpload = () => {
const { uploadFile, uploading, progress, error } = useUploadFile();
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files ? event.target.files[0] : null;
if (file) {
uploadFile(file);
}
};
return (
<div>
<input type="file" onChange={handleFileChange} />
{uploading && (
<div>
<progress value={progress} max={100} />
<span>{progress}%</span>
</div>
)}
{error && <div style={{ color: 'red' }}>{error}</div>}
</div>
);
};
export default FileUpload;
🧠 解释
-
useUploadFile
:这个自定义 Hook 包含了所有上传逻辑,包括:- 初始化上传(
initiateUpload
) - 获取签名 URL(
getSignedUrl
) - 上传分片(
uploadChunk
) - 完成上传(
completeUpload
)
- 初始化上传(
-
uploadFile
:- 将文件分块,每块最大 5MB。
- 上传每个块,上传完成后更新进度条。
- 上传完成后,调用合并接口(
completeUpload
)合并文件。
-
FileUpload
:- 用于处理用户上传文件的界面。
- 显示上传进度条。
🧠 注意事项
- 签名 URL :需要确保后端
GET /upload/signed-url
返回的是有效的上传链接。 - ETag :这里为了简化,假设返回的
ETag
为'dummyETag'
,你可以从 S3 直接获取真实的ETag
。 - 重试逻辑:为了避免上传失败,设置了重试机制(最大重试次数:3次)。