Spring Boot升级后application/octet-stream不支持问题排查
将Spring Boot从2.7.4版本升级到3.1.5后,出现org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported错误,升级前所有功能运行正常。技术栈为Angular前端 + OpenAPI Generator 7.0.0生成前后端代码。
OpenAPI YAML配置
/testdimension/file-upload: post: tags: - TestDimension operationId: addFilesToTestExecution summary: Send files and IDs to the backend requestBody: required: true content: multipart/form-data: schema: type: object properties: toolId: type: integer format: int64 ids: type: array items: type: integer format: int64 description: test dimensions array files: type: array items: type: string format: binary description: test files to upload required: - ids - files responses: '200': description: OK '400': description: Invalid request payload '500': description: Internal server error
生成的Spring接口类代码
@Operation( operationId = "addFilesToTestExecution", summary = "Send files and IDs to the backend", tags = { "TestDimension" }, responses = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "400", description = "Invalid request payload"), @ApiResponse(responseCode = "500", description = "Internal server error") } ) @RequestMapping( method = RequestMethod.POST, value = "/testdimension/file-upload", consumes = "multipart/form-data" ) default ResponseEntity<Void> addFilesToTestExecution( @Parameter(name = "ids", description = "test dimensions array", required = true) @RequestPart(value = "ids", required = true) List<Long> ids, @Parameter(name = "files", description = "test files to upload", required = true) @RequestPart(value = "files", required = true) List<MultipartFile> files, @Parameter(name = "toolId", description = "") @RequestParam(value = "toolId", required = false) Long toolId ) { return getDelegate().addFilesToTestExecution(ids, files, toolId); }
生成的Angular请求代码
/** * Send files and IDs to the backend * @param ids test dimensions array * @param files test files to upload * @param toolId * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ public addFilesToTestExecution(ids: Array<number>, files: Array<Blob>, toolId?: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined}): Observable<any>; public addFilesToTestExecution(ids: Array<number>, files: Array<Blob>, toolId?: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined}): Observable<HttpResponse<any>>; public addFilesToTestExecution(ids: Array<number>, files: Array<Blob>, toolId?: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined}): Observable<HttpEvent<any>>; public addFilesToTestExecution(ids: Array<number>, files: Array<Blob>, toolId?: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined}): Observable<any> { if (ids === null || ids === undefined) { throw new Error('Required parameter ids was null or undefined when calling addFilesToTestExecution.'); } if (files === null || files === undefined) { throw new Error('Required parameter files was null or undefined when calling addFilesToTestExecution.'); } let headers = this.defaultHeaders; let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; if (httpHeaderAcceptSelected === undefined) { // to determine the Accept header const httpHeaderAccepts: string[] = [ ]; httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); } if (httpHeaderAcceptSelected !== undefined) { headers = headers.set('Accept', httpHeaderAcceptSelected); } // to determine the Content-Type header const consumes: string[] = [ 'multipart/form-data' ]; const canConsumeForm = this.canConsumeForm(consumes); let formParams: { append(param: string, value: any): any; }; let useForm = false; let convertFormParamsToString = false; // use FormData to transmit files using content-type "multipart/form-data" // see https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data useForm = canConsumeForm; if (useForm) { formParams = new FormData(); } else { formParams = new HttpParams({encoder: this.encoder}); } if (toolId !== undefined) { formParams = formParams.append('toolId', <any>toolId) as any || formParams; } if (ids) { if (useForm) { ids.forEach((element) => { formParams = formParams.append('ids', <any>element) as any || formParams; }) } else { formParams = formParams.append('ids', ids.join(COLLECTION_FORMATS['csv'])) as any || formParams; } } if (files) { if (useForm) { files.forEach((element) => { formParams = formParams.append('files', <any>element) as any || formParams; }) } else { formParams = formParams.append('files', files.join(COLLECTION_FORMATS['csv'])) as any || formParams; } } let responseType: 'text' | 'json' = 'json'; if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) { responseType = 'text'; } return this.httpClient.post<any>(`${this.configuration.basePath}/testdimension/file-upload`, convertFormParamsToString ? formParams.toString() : formParams, { responseType: <any>responseType, withCredentials: this.configuration.withCredentials, headers: headers, observe: observe, reportProgress: reportProgress } ); }
已尝试修改YAML中的Content-Type配置、在Spring配置中启用octet-stream,均未解决问题,询问错误来自前端还是后端。
这个错误来自后端,核心原因是Spring Boot 3.x(基于Spring Framework 6.x)对Multipart请求的校验逻辑大幅收紧,结合你的代码细节,具体原因和解决方法如下:
核心原因
- Spring接口用
@RequestPart接收ids数组,但Angular生成的代码是逐个向FormData中appendids参数,导致该部分的Content-Type默认是application/octet-stream; - Spring Boot 2.7.x对这种非预期媒体类型的兼容性更强,会自动转换,但3.x不再支持这种自动兼容,直接抛出媒体类型不支持的异常。
解决方法
方法1:修改OpenAPI YAML,规范数组参数的媒体类型
在ids字段定义中添加contentType,明确指定为application/json,让生成的代码以JSON格式传递数组:
ids: type: array items: type: integer format: int64 description: test dimensions array contentType: application/json # 新增该行
重新生成前后端代码后,Angular会将整个ids数组作为JSON字符串传入,Spring可正确识别媒体类型并解析为List<Long>。
方法2:修改Spring接口,兼容现有前端请求
如果不想修改前端代码,可将@RequestPart改为@RequestParam接收数组:
@Parameter(name = "ids", description = "test dimensions array", required = true) @RequestParam(value = "ids", required = true) List<Long> ids,
@RequestParam会自动解析FormData中多个同名的ids参数为数组,无需关心单个参数的Content-Type。
方法3:全局配置Spring放宽媒体类型校验
在Spring Boot配置类中添加消息转换器,允许application/octet-stream转换为目标类型:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setSupportedMediaTypes(Arrays.asList( MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM )); converters.add(converter); } }
此方法为全局兼容方案,适合不想修改业务代码的场景,但会降低接口的校验严格性。
内容的提问来源于stack exchange,提问作者Maks

