@senspond
>
Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법을 정리하고 multipart/formdata 수동전송 방법을 정리한 내용입니다.
form 태그를 이용한 파일 업로드 가장 기본적인 예제이다.
프론트엔드
<form enctype="multipart/form-data" method="post" action="/api/uploads" >
<input type="file" title="" id="file" name="file">
<input type="submit" value="전송"/>
</form>
koa 백엔드 서버
router.post("/uploads", upload.single('file'), async (ctx, next)=>{
console.log(ctx.request.file)
ctx.response.body = {
filename: ctx.request.file.filename,
};
})
하지만 아래처럼 사용하게 되면 문제가 발생 할 수가 있다.
- 클라이언트: fetch API 를 통해 multipart/form-data 로 파일 전송
- 서버: Node.js에서 Multer를 통해 파일 업로드 처리
const formData = new FormData();
formData.append('imgFile', imgFile.current);
const response = await fetch('/upload', {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data'
},
body: formData
});
Multipart: Boundary not found 오류가 발생한다.
headers: {
'Content-Type': 'multipart/form-data'
},
axios 를 사용 할 때는 'Content-Type' 에 'multipart/form-data'를 넣어주어도 이런 오류가 발생하지 않는데, fetch 를 사용하면 오류가 발생한다.
fetch 사용시, headers 항목을 비우고 body에 FormData객체를 담아 전송하면 된다.
const formData = new FormData();
formData.append('imgFile', imgFile.current);
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
디버깅을 해보면 boundary 가 들어간것을 볼 수가 있다.
웹 개발을 하다보면 간혹 Server to Server 서버에서 다른 서버로 파일을 전송해야 하는 경우가 있다. 그런 경우 프로토콜에 맞게 작성해서 보내줘야 한다. 예를 들자면, nodejs 백엔드 서버에서 Spring 백엔드 서버로 파일을 전송 하는 경우 등이 있을 수 있다.
HTTP요청의 multipart/formdata를 수동으로 구성하기 위해서는
헤더에 Content-type=multipart/formdata으로 콘텐츠타입을 주고, 이 때 각 파트간의 경계를 위한 구분자를 추가한다.. Content-type=multipart/formdata; boundary=--....
구분자는 반드시 --
으로 시작해야 한다.
각각의 파트에는 헤더가 들어가며, Content-Disposition: form-data; name=" ... "
과 같은 폼 필드의 정보를 추가합한다.
모든 파트가 끝나면 구분자 뒤에 --
을 추가해 준다.
예시는 자바 서버에서 다른 외부 서버로 파일전송 코드이다.
예전에 Java 프로젝트를 하며 만들어 둔 클래스가 있어서 정리해봅니다.
@Slf4j
public class HttpMultiPartUtils {
private static final String LINE = "\r\n";
private final String boundary;
private final HttpURLConnection httpConn;
private final String charset;
private final OutputStream outputStream;
private final PrintWriter writer;
public HttpMultiPartUtils(String requestURL, String charset, Map<String, String> headers) throws IOException {
this.charset = charset;
this.boundary = UUID.randomUUID().toString();
URL url = new URL(requestURL);
httpConn = (HttpURLConnection) url.openConnection();
httpConn.setUseCaches(false);
httpConn.setDoOutput(true);
httpConn.setDoInput(true);
httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
if (headers != null && headers.size() > 0) {
for (String key : headers.keySet()) {
String value = headers.get(key);
httpConn.setRequestProperty(key, value);
}
}
this.outputStream = httpConn.getOutputStream();
this.writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true);
}
public void addFilePart(String fieldName, File uploadFile)
throws IOException {
String fileName = uploadFile.getName();
writer.append("--").append(boundary).append(LINE);
writer.append("Content-Disposition: form-data; name=\"").append(fieldName).append("\"; filename=\"").append(fileName).append("\"").append(LINE);
writer.append("Content-Type: ").append(URLConnection.guessContentTypeFromName(fileName)).append(LINE);
writer.append("Content-Transfer-Encoding: binary").append(LINE);
writer.append(LINE);
writer.flush();
FileInputStream inputStream = new FileInputStream(uploadFile);
byte[] buffer = new byte[4096];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
inputStream.close();
writer.append(LINE);
writer.flush();
}
public void addFormField(String name, String value) {
writer.append("--").append(boundary).append(LINE);
writer.append("Content-Disposition: form-data; name=\"").append(name).append("\"").append(LINE);
writer.append("Content-Type: text/plain; charset=").append(charset).append(LINE);
writer.append(LINE);
writer.append(value).append(LINE);
writer.flush();
}
public String finish() throws IOException {
String response = "";
writer.flush();
writer.append("--").append(boundary).append("--").append(LINE);
writer.close();
// checks server's status code first
int status = httpConn.getResponseCode();
if (status == HttpURLConnection.HTTP_OK) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = httpConn.getInputStream().read(buffer)) != -1) {
result.write(buffer, 0, length);
}
response = result.toString(this.charset);
httpConn.disconnect();
} else {
throw new IOException();
}
return response;
}
}
사용예시
public ImageUploadResDto uploadImageToServer(MultipartFile file, String paths, Double ratio) throws IOException {
Map<String, String> headers = new HashMap<>();
HttpMultiPartUtils multipart = new HttpMultiPartUtils(uploadServer + "/upload/image", "utf-8", headers);
multipart.addFormField("paths", paths);
multipart.addFilePart("file", convertFile(file));
multipart.addFormField("ratio", String.valueOf(ratio));
return mapper.readValue(multipart.finish(), ImageUploadResDto.class);
}
글을 쓰고 보니 프론트엔드 / 백엔드 카테고리를 나눠서 구분하기가 애매모호한 글이 되버린 것 같네요. 요즘 텐서플로우 JS 를 공부 중인데... 프론트엔드에서 백엔드의 전유물로만 생각되었던 머신러닝을 해버리니 묘하더라고요. 그것도 마찬가지인것 같습니다 ㅎㅎ
안녕하세요. Red, Green, Blue 가 만나 새로운 세상을 만들어 나가겠다는 이상을 가진 개발자의 개인공간입니다.
현재글에서 작성자가 발행한 같은 카테고리내 이전, 다음 글들을 보여줍니다
@senspond
>