Apollo Supergraph调用Subgraph传递Upload标量类型失败问题
解决Apollo Supergraph转发Upload类型请求到gqlgen Subgraph的错误问题
问题分析
Apollo Supergraph(Node.js网关)默认会解析前端发送的multipart格式文件上传请求,将Upload类型解析为内部对象后以JSON格式转发给Go语言gqlgen编写的Subgraph。但gqlgen的Subgraph期望接收符合GraphQL multipart规范的请求,而非JSON结构中的普通对象,因此抛出错误"message": "map[string]interface {} is not an Upload"。直接调用Subgraph正常,说明Subgraph本身的文件上传逻辑无问题,问题出在Supergraph的请求转发处理上。
可行解决方案
方案一:配置Supergraph透传multipart请求(推荐)
让Supergraph不对文件上传请求做解析,直接将原始的multipart请求体转发给对应Subgraph,保留请求的原始格式,符合gqlgen的接收要求。
- 修改Apollo Gateway的服务构建逻辑:
const { ApolloGateway } = require('@apollo/gateway'); const { fetch } = require('cross-fetch'); const gateway = new ApolloGateway({ serviceList: [/* 填入你的Subgraph服务列表,例如{ name: 'upload-service', url: 'http://your-subgraph-url' } */], buildService({ name, url }) { return { async request({ request, context }) { // 判断是否为文件上传的Mutation请求 const isUploadRequest = request.query.includes('mutation') && request.query.includes('Upload'); if (isUploadRequest && context.req) { // 透传原始multipart请求 const response = await fetch(url, { method: 'POST', headers: { ...context.req.headers, // 强制保留multipart请求的Content-Type 'Content-Type': context.req.headers['content-type'], }, body: context.req.body, }); return response.json(); } // 非上传请求按默认逻辑处理 return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), }).then(res => res.json()); }, }; }, });
- 将原始请求对象传入Context:
在Apollo Server的配置中,把前端请求的req对象注入到context中,让网关能获取到原始请求体:
const { ApolloServer } = require('apollo-server'); const server = new ApolloServer({ gateway, context: ({ req }) => ({ req }), }); server.listen().then(({ url }) => { console.log(`Supergraph running at ${url}`); });
方案二:修改gqlgen Subgraph兼容JSON格式的Upload对象
如果无法修改Supergraph配置,可以在Subgraph端自定义Upload类型的解析器,将Supergraph转发的JSON结构转换为gqlgen可识别的类型。
- 配置gqlgen自定义解析器:
在gqlgen.yml中指定Upload类型的自定义解析逻辑:
models: Upload: model: github.com/99designs/gqlgen/graphql.Upload
- 编写自定义解析函数:
package graph import ( "context" "mime/multipart" "os" "github.com/99designs/gqlgen/graphql" ) // 解析Supergraph转发的JSON格式Upload对象 func UnmarshalUpload(v interface{}) (multipart.File, *multipart.FileHeader, error) { fileData, ok := v.(map[string]interface{}) if !ok { return nil, nil, graphql.NewError("invalid Upload format from Supergraph") } fileInfo, ok := fileData["file"].(map[string]interface{}) if !ok { return nil, nil, graphql.NewError("missing file info in Upload") } // 此处需要注意:Supergraph转发时仅传递了文件元数据,未传递文件内容 // 若要使用此方案,需修改Supergraph在转发时同时传递文件的base64内容,再在此处解码为临时文件 // 示例(假设Supergraph添加了content字段): // contentBase64 := fileInfo["content"].(string) // content, _ := base64.StdEncoding.DecodeString(contentBase64) // tempFile, _ := os.CreateTemp("", "upload-*") // tempFile.Write(content) // tempFile.Seek(0, 0) // fh := &multipart.FileHeader{Filename: fileInfo["filename"].(string)} // return tempFile, fh, nil // 注:此方案需Supergraph配合修改,复杂度较高,优先推荐方案一 return nil, nil, graphql.NewError("file content not found in Upload data") } func MarshalUpload(f multipart.File, fh *multipart.FileHeader) interface{} { return map[string]interface{}{ "file": map[string]interface{}{ "filename": fh.Filename, "mimetype": fh.Header.Get("Content-Type"), "encoding": "7bit", }, } }
方案三:预上传文件至S3,传递URL替代Upload类型
绕开GraphQL的multipart上传机制,先将文件上传至S3等存储服务,再将文件URL作为字符串参数传递给Supergraph,Subgraph通过URL下载文件处理。
- 前端流程:
- 调用后端接口获取S3预签名上传URL
- 将文件上传至S3,获取文件的公开访问URL
- 调用GraphQL Mutation,将URL作为参数传入
- Subgraph处理:
接收URL参数后,发起HTTP请求下载文件,进行后续业务处理。
此方案无需处理GraphQL Upload类型的兼容性问题,但需要额外的存储服务配置和前端逻辑改造。
内容的提问来源于stack exchange,提问作者mbx-mbx
相关产品推荐
相关产品推荐

