NestJS Axios拦截器处理文件上传请求时Token刷新后请求无限挂起的问题排查
NestJS Axios拦截器处理文件上传请求时Token刷新后请求无限挂起的问题排查
嘿,我仔细看了你的问题和代码,这个无限挂起的坑其实挺常见的——核心问题出在FormData里的文件流是一次性的,咱们一步步拆解解决:
问题根源分析
当你用fs.createReadStream()创建文件流并塞进FormData时,这个流只能被读取一次。第一次请求因为Token过期失败后,拦截器直接复用了原来的originalRequest,但此时FormData里的文件流已经被消费空了,Axios一直在等流返回数据,自然就无限卡住了。
另外还有个小细节:你的拦截器类声明了要实现NestInterceptor接口,但没写intercept方法,不过这个是次要问题,先解决流的复用才是关键。
具体解决方案
咱们需要在重试时重新生成FormData和文件流,具体分两步改:
1. 给文件上传请求添加可复用的FormData生成逻辑
在发起上传请求时,不要直接把FormData实例传给Axios,而是存一个生成FormData的函数,这样重试时能重新生成新的实例和流:
async uploadAttachmentToDummyEntry(DummyEntryId: number, attachmentFilepath: string) { this.logger.log("Uploading attachment to Dummy Entry started..."); // 定义生成FormData的函数,每次调用都会创建新的流 const createFormData = () => { let uploadAttachmentFormData = new FormData(); uploadAttachmentFormData.append("attachment[file]", fs.createReadStream(attachmentFilepath)); return uploadAttachmentFormData; }; const config = { maxBodyLength: 100 * 1024 * 1024, // 100 MB in bytes maxContentLength: 100 * 1024 * 1024, // 100 MB in bytes headers: { "Accept": "application/json", // 别手动写Content-Type!FormData会自动生成带boundary的正确值 }, // 把生成函数存到自定义字段里,方便拦截器取用 _formDataCreator: createFormData }; // 第一次请求时生成FormData,并自动获取正确的headers const formData = createFormData(); config.headers = { ...config.headers, ...formData.getHeaders() }; const asd = await firstValueFrom( this.http.post( `${this.configService.get("Dummy_API_URL")}/invoices/${DummyEntryId}/attachments`, formData, config ) ).catch((error) => { console.log(error); return error; }); return asd; }
2. 修改拦截器的重试逻辑,重新生成请求体
在拦截器的错误处理里,判断如果是文件上传请求(有_formDataCreator字段),就重新生成FormData和headers再重试:
@Injectable() export class DummyInterceptor implements NestInterceptor { // 必须实现接口才能生效 private readonly logger = new Logger(DummyInterceptor.name); DummyApiBearerToken: string; constructor( private httpService: HttpService, private DummyService: DummyService, private configService: ConfigService ) { this.httpService.axiosRef.interceptors.request.use((config) => { console.log("Axios request interceptor"); if (config.url.startsWith(this.configService.get("Dummy_API_URL"))) { config.headers["Authorization"] = "Bearer " + this.DummyApiBearerToken; config.headers["Accept"] = "application/json"; console.log(config.headers); } return config; }); this.httpService.axiosRef.interceptors.response.use((response) => { console.log("Axios response interceptor"); console.log(response.data); return response; }, async (error) => { this.logger.log("Dummy API error interceptor"); if (error.response?.data) { this.logger.error(error.response.data); } const originalRequest = error.config; if (error.response?.status === HttpStatusCode.Unauthorized && !originalRequest._retry) { this.logger.log("Unauth, refreshing Dummy bearer"); originalRequest._retry = true; const response = await this.DummyService.getNewBearerToken(); this.DummyApiBearerToken = response.data["access_token"]; // 处理文件上传的重试:重新生成FormData和headers if (originalRequest._formDataCreator) { const newFormData = originalRequest._formDataCreator(); originalRequest.data = newFormData; originalRequest.headers = { ...originalRequest.headers, ...newFormData.getHeaders() }; } return this.httpService.axiosRef(originalRequest); } return Promise.reject(error); }); } // 实现intercept方法,Nest才能识别这是个拦截器 intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle(); } }
额外提醒
- 绝对不要手动设置
multipart/form-data的Content-Type,FormData会自动生成带boundary的正确值,手动写会导致后端解析失败。 - 确保拦截器正确实现了
NestInterceptor的intercept方法,否则Nest不会加载这个拦截器。
这样修改后,Token过期重试时会重新创建文件流和FormData,请求就能正常发送完成,不会再无限挂起了。
备注:内容来源于stack exchange,提问作者Dev
相关产品推荐
相关产品推荐

