遭遇した事象
クライアントからサーバーに画像ファイルをアップロードするAPIで、クライアントで画像ファイルをbase64にエンコードしてAPIのリクエストとして送信する処理がある
const reader = new FileReader();
reader.onload = (e) => {
let base64 = e.target.result;
const requestBody = {
file: base64,
}
axios.post("/api/upload", requestBody, {
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
// 事後処理
}
}
これを受けてバックエンド(laravelで構築)では下記のデコード後にMIMEを調べて画像じゃなかったらエラーを返すという処理をしている
$file = base64_decode($request->file);
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo,$file);
if(strpos($mime,'image/') !== false){
// ファイルを保存する
}
else{
// エラー処理
}
ところが実際に動かして画像ファイルをアップロードすると、画像と判定されないエラーが出てアップロードができなかった。
調査と推測
実際にMIMEがバックエンドでどうなっているのかを調べてみたところ、「application/octet-stream」になっていることがわかった。
また、バックエンドの単体テストでは正しく判定できていため、クライアントサイドに問題があるのではと考えた・
そこで、クライアント側でbase64エンコードした内容をチェックすると、
data:image/png;base64,iVBORw0KG・・・
となっていたが、バックエンドの単体テストで使ったbase64の文字列を確認すると、
iVBORw0KG・・・
と、「data:image/png;base64,」の部分が無かった。
対処
上記の事から「data:image/png;base64,」を消して送ればいいだろう、ということでクライアント側のコードを下記のように修正した。
const reader = new FileReader();
reader.onload = (e) => {
let base64 = e.target.result;
// base64,の次の文字から取得して、それ以前を削除した
const base64Index = base64.indexOf("base64,");
if (base64Index !== -1) {
// 「base64,」までで7文字なのでそれ以降を取得する
base64 = base64.substring(base64Index + 7);
}
const requestBody = {
file: base64,
}
axios.post("/api/upload", requestBody, {
headers: {
"Content-Type": "application/json",
},
})
.then((res) => {
// 事後処理
}
}
これによって無事にMIMEを判定してアップロードをすることができた。
補足
base64エンコードにおいて「data:image/png;base64,」の部分はエンコードされたバイナリデータそのものではなく、エンコードされたバイナリデータのメタ情報を表すものなのでそれを含めてデコードを行い、MIMEを取得すると正しく取得できないことがある、というのが原因のようだった。
今回はクライアンド側でメタ情報を消して送信しているが、サーバサイド側で処理後にエンコードするということもできる。
<?php
// 例として、$request->fileにBase64エンコードされたデータが含まれているとします
$encodedFile = $request->file;
// プレフィックスを除去する
// preg_replaceを使用して、data:image/png;base64, などのプレフィックス部分を空文字に置き換えます
$base64Data = preg_replace('#^data:image/\w+;base64,#i', '', $encodedFile);
// Base64デコードを行う
$decodedFile = base64_decode($base64Data);
// MIMEタイプを調べる
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $decodedFile);
finfo_close($finfo);
// MIMEタイプが画像かどうかをチェック
if (strpos($mime, 'image/') === 0) {
// ファイルを保存する
} else {
// エラー処理
}