注意
fetchのpolyfillとしてnode-fetchを使っていると(next.jsのapi routesは使ってる)
Note: when body is a Stream, Content-Length is not set automatically.
なので、fs.createReadStream
なファイルを渡すと、結果としてcontent-lengthがheaders情報としてサーバー側に渡されない。 サーバーによってはエラーの原因になると思う。
puma v4.3.7未満はcontent-lengthが無いとPOSTデータを上手く処理できない様子なので、うまくサーバーにデータが行かない場合はpumaのバージョンを確認したほうが良い。
Backport set CONTENT_LENGTH for chunked requests
問題
- apollo clientを使っている
- apollo-upload-clientを使っている
- node環境から使っている(nextjsのapi routesとか)
この場合、clientと同じようなコードを書いてもアップロードが機能しない。
解決方法
node環境では
- Fileがない
- Blobがない
- FormDataがない
- fetchがない(nextjsならpolyfillしてるけど)
なので、apollo-upload-clientのcreateUploadLinkを使う際にこれらのnode版を指定してやる。
そしてFile、Blobがないのでfsでファイルをstreamとして読み込み、それをアップロード対象のオブジェクトとするが、デフォルトではFileかBlobしか対象になっていないので、ReadStreamが対象になるようにisExtractableFile
で設定し直す。
import FormData from "form-data" // yarn add form-data
import fetch from "isomorphic-unfetch" // yarn add isomorphic-unfetch
import { ReadStream } from "fs"
import { createUploadLink, isExtractableFile } from "apollo-upload-client"
createUploadLink({
uri: "https://backend/graphql",
fetch,
FormData,
isExtractableFile: (value) =>
isExtractableFile(value) ||
(typeof ReadStream !== "undefined" && value instanceof ReadStream),
})
import { ApolloClient, InMemoryCache } from "@apollo/client"
import { createUploadLink, isExtractableFile } from "apollo-upload-client"
import FormData from "form-data"
import fs, { ReadStream } from "fs"
import type { NextApiRequest, NextApiResponse } from "next"
import * as Generated from "src/generated/graphql" // @graphql-codegenを使っているので...
const client = new ApolloClient({
cache: new InMemoryCache(),
link: createUploadLink({
uri: "https://backend/graphql", // 他のgraphqlサーバー
// fetch, nextjsを使っているので必要ない
FormData,
isExtractableFile: (value) =>
isExtractableFile(value) ||
(typeof ReadStream !== "undefined" && value instanceof ReadStream),
}),
})
export default async (req: NextApiRequest, res: NextApiResponse) => {
const file = fs.createReadStream(fs.realpathSync("./test.jpg"))
await client.mutate<
Generated.TestMutationMutation,
Generated.TestMutationMutationVariables
>({
mutation: Generated.TestMutationDocument,
variables: {
input: {
file,
},
},
fetchPolicy: "no-cache",
})
res.status(200).end()
}
{
"scripts": {
"dev": "next",
"generate": "graphql-codegen -w --config codegen.yml"
},
"dependencies": {
"@apollo/client": "^3.4.16",
"apollo-upload-client": "^16.0.0",
"form-data": "^4.0.0",
"graphql": "^15.6.1",
"next": "^11.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.2.1",
"@graphql-codegen/typescript": "^2.2.4",
"@graphql-codegen/typescript-operations": "^2.1.8",
"@graphql-codegen/typescript-react-apollo": "^3.1.6",
"@types/react": "^17.0.30",
"typescript": "^4.4.4"
}
}