この記事を読んだ方がいい人
Javascriptに不慣れな人。
HTMLで当たり前のように遅れていた画像がBFFを通したら送れなくて困っている人
この記事を読まない方がいい人
コーディングとネットワーク領域の両方に自信があるひと
やりたかったこと
Next.jsで作ったフロントアプリケーションにて画像登録のフォームを作成した。
バックエンドAPIはGoで作ってある
はまったポイント
BFFで画像をハンドリングする処理がうまくいかない。
そもそもフォームデータって
フォームって一言で言っても画面UIとしての入力フォーム的なこととも言えるし、もう少し物理層チックなことを言ってそうな気もしますが、今回はHTTP通信においてのテキストとしてのフォームのことです。
フォームをブラウザから送信すると中身はこんな感じの区切り文字と値で表されるテキストになっています。
------WebKitFormBoundaryH1hEBwyt0daM3FqD
Content-Disposition: form-data; name="profileImage"; filename="Screenshot 2024-00-00 at 00.55.43.png"
Content-Type: image/png
------WebKitFormBoundaryH1hEBwyt0daM3FqD
Content-Disposition: form-data; name="email"
mail.address@example.com
------WebKitFormBoundaryH1hEBwyt0daM3FqD
Content-Disposition: form-data; name="password"
12345abcde
------WebKitFormBoundaryH1hEBwyt0daM3FqD
Content-Disposition: form-data; name="birthOfDate"
・
・
busboyが使えない
busboyというのはどんな少年なのかと思った方もいるかもですが、Javascriptでmultipartのリクエストを解釈するためのライブラリです。
本気の情報をお求めの方は公式へどうぞ。
最近awaitとかasyncとかPromiseし始めたJS素人童貞の自分からすると、このコールバック祭りなコードはハードルが高かったです。
受け取ったフォームデータの処理を定義しておいて、req.pipeに渡す処理。
function processFiles(req: NextApiRequest): Promise<any> {
return new Promise((resolve, reject) => {
const bb = new Busboy({ headers: req.headers });
const result = { files: [], fields: {} };
bb.on('file', (fieldname, file, filename, encoding, mimetype) => {
console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding}, mimetype: ${mimetype}`);
const buffers = [];
file.on('data', (data) => {
buffers.push(data);
});
file.on('end', () => {
result.files.push({
fieldname,
filename,
encoding,
mimetype,
data: Buffer.concat(buffers), // ファイルデータを結合
});
});
});
bb.on('field', (fieldname, val) => {
result.fields[fieldname] = val;
});
bb.on('finish', () => {
resolve(result);
});
bb.on('error', (error) => {
reject(error);
});
req.pipe(bb);
});
このライブラリはUnexpected EOF的なエラーが解消できず使うのを断念しました。
フォームを受け取る前に次の処理が始まっていそうにも見えたのですが覚えたてのasync awaitをこねくり回してもダメだったので一旦捨てました。
つまりフロントエンド→BFFで止まった状態です。
formidableでBFFを突破
ライブラリを変えてformidableで実装し直し。
return await new Promise((resolve, reject) => {
// formidableを使用してファイルとフィールドをパース
const form = new IncomingForm()
form.parse(req, async (err, fields, files) => {
if (err) return reject(err)
const file = files.profileImage[0]
const fileContent = await fs.readFile(file.filepath)
const formData = new FormData()
// 画像の場合はファイル名とMIMEタイプを指定しないとバックエンドで正しく読み込めない
formData.append('userProfileImage', fileContent)
formData.append('email', fields.email[0])
formData.append('password', fields.password[0])
これでGoのAPIまで到達したことは確認できました。
でもデバッグすると画像の実態は届いていそうなのですが、画像を取り扱おうとすると画像がない状態に。
file, err := c.FormFile("profileImage") // これがエラーになる
リクエスト情報をデバッグするとバイナリ文字がブァーっと出力されるので実態はきていそうなのだが。
ちなみにPostmanからは正常に送れ登録できるのでクライアント側に原因がありそう。
解決
return await new Promise((resolve, reject) => {
// formidableを使用してファイルとフィールドをパース
const form = new IncomingForm()
form.parse(req, async (err, fields, files) => {
if (err) return reject(err)
const file = files.profileImage[0]
const fileContent = await fs.readFile(file.filepath)
const formData = new FormData()
// 画像の場合はファイル名とMIMEタイプを指定しないとバックエンドで正しく読み込めない
// appendの第3引数!!!!
formData.append('userProfileImage', fileContent, {
filename: 'userProfileImage.jpg', // これとか
// contentType: type.mime, // 指定する場合は動的に取得する // これはなくてもよかた。
knownLength: fileContent.length // これとか
})
formData.append('email', fields.email[0])
formData.append('password', fields.password[0])
formにappendする際、付加情報みたいなものを第3引数に取れるようなのですが、これを追加することで正しくハンドリングできました。
contentTypeは画像データから動的にimage/jpeg
だのimage/png
だのやるのがだるかったので消してみたら動きました。
ちなみに頼りになるパイセンに相談した結果この答えまで辿り着いたのですが、ここが怪しいって微塵も想像してなかったので実力不足を突きつけられたお題でした。。