Ferry GraphQL+Flutter上传文件报MultipartFile编码转换失败错误
问题原因
抛出GqlException(LinkException(DioError [DioErrorType.other]: Converting object to an encodable object failed: Instance of 'MultipartFile'错误的核心原因有两个:
- 自定义
UploadSerializer的serialize方法直接返回Dio的MultipartFile实例,Ferry默认的JSON序列化逻辑会尝试将所有变量值转换为JSON支持的基础类型(字符串、数字、布尔、列表、Map),无法识别MultipartFile实例直接触发类型转换错误。 - 未配置支持GraphQL Multipart上传规范的请求链路,默认的Dio/HTTP Link只会将请求序列化为纯JSON格式发送,不会自动将文件字段封装为
multipart/form-data格式的请求体,不符合文件上传的接口要求。
修复方案
1. 修正UploadSerializer逻辑
自定义序列化器遇到MultipartFile类型时,不要直接返回实例本身,返回一个可JSON序列化的特殊标记,避免触发默认JSON序列化报错:
import 'package:dio/dio.dart' show MultipartFile; import 'package:built_value/serializer.dart'; class UploadSerializer extends PrimitiveSerializer<MultipartFile> { // 用来标记文件字段的特殊字符串,不会和普通业务值冲突 static const _fileMarker = '__GRAPHQL_UPLOAD_FILE__'; @override MultipartFile deserialize( Serializers serializers, Object serialized, { FullType specifiedType = FullType.unspecified, }) { assert(serialized is List<int>, "UploadSerializer expected 'List<int>' but got ${serialized.runtimeType}"); return MultipartFile.fromBytes(serialized as List<int>); } @override Object serialize( Serializers serializers, MultipartFile file, { FullType specifiedType = FullType.unspecified, }) { // 返回可JSON编码的标记,后续链路会识别该标记替换为实际文件 return _fileMarker; } @override Iterable<Type> get types => [MultipartFile]; @override String get wireName => "Upload"; }
2. 添加Multipart上传转换链路
在Ferry的请求链路最前端加一个转换Link,识别请求中的文件标记,按照GraphQL Multipart Request规范构造FormData请求体:
import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:gql/ast.dart'; import 'package:gql/language.dart'; import 'package:ferry/ferry.dart'; import 'package:gql_dio_link/gql_dio_link.dart'; Link multipartUploadLink() { return Link((request, [forward]) async* { final vars = request.variables.toJson(); final Map<String, MultipartFile> files = {}; final marker = UploadSerializer._fileMarker; // 递归遍历变量,提取所有MultipartFile并记录路径 Object? processValue(Object? value, String currentPath) { if (value == marker) { // 按路径从原始变量中拿到真实的MultipartFile实例 final pathParts = currentPath.split('.'); dynamic realValue = request.variables.toJson(); for (final part in pathParts) { if (realValue is List) { realValue = realValue[int.parse(part)]; } else { realValue = realValue[part]; } } files[currentPath] = realValue as MultipartFile; // 按照GraphQL上传规范,operations中文件对应位置设为null return null; } if (value is Map<String, dynamic>) { return value.map((k, v) => MapEntry(k, processValue(v, '$currentPath.$k'))); } if (value is List) { return value.asMap().entries.map((e) => processValue(e.value, '$currentPath.${e.key}')).toList(); } return value; } // 处理根级变量 final processedVars = <String, dynamic>{}; vars.forEach((key, value) { processedVars[key] = processValue(value, key); }); // 没有文件直接走原有请求逻辑 if (files.isEmpty) { yield* forward!(request); return; } // 构造符合GraphQL上传规范的FormData final fileMap = <String, List<String>>{}; final fileFields = <String, MultipartFile>{}; var fileIndex = 0; files.forEach((path, file) { fileMap[fileIndex.toString()] = ['variables.$path']; fileFields[fileIndex.toString()] = file; fileIndex++; }); final formData = FormData.fromMap({ 'operations': json.encode({ 'query': printNode(request.operation.document as DocumentNode), 'variables': processedVars, }), 'map': json.encode(fileMap), ...fileFields, }); // 构造新的请求,携带FormData final modifiedReq = request.rebuild((b) => b ..context = Context.fromList([ DioLinkContext( data: formData, headers: {'Content-Type': 'multipart/form-data'}, ), ])); yield* forward!(modifiedReq); }); }
3. 调整客户端链路配置
将上面实现的上传转换Link放到链路最前端,再拼接DioLink:
final ferryClient = Client( link: multipartUploadLink().concat( DioLink( '你的GraphQL接口地址', dio: dioInstance, // 提前初始化好的Dio实例 ), ), // 其余cache、serializer配置保持原有内容不变 );
4. 重新生成代码
所有配置修改完成后,执行build_runner命令重新生成类型和序列化代码:
dart run build_runner build --delete-conflicting-outputs
优化建议
- 生成MultipartFile时主动传入filename参数,避免后端无法识别文件名:
final multipartFile = await MultipartFile.fromFile( XFile.path, filename: XFile.name, );
- 如果后端有文件大小、类型限制,提前在Dio拦截器或者上传前做校验,减少无效请求。
内容的提问来源于stack exchange,提问作者Patttt
相关产品推荐
相关产品推荐

