You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:42:23