HTML form태그의 action 요청으로 multipart/formdata 형식으로 다중파일과 본문 내용을 전달 했었는데,
JavaScript 의 fetch API 및 FormData 를 활용한 비동기요청으로 변환하는 과정에서 발생한 에러 기록들을 남겨보았습니다.
Javascript
let postFileData = async (url, data = {}, csrf_header, csrf_token) => {
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE 등
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "multipart/form-data",
header: csrf_header,
'X-CSRF-Token': csrf_token
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: data
});
return response.json(); // JSON 응답을 네이티브 JavaScript 객체로 파싱
}
let content = form.querySelector(".question-input").value; // 요청 글
let files = document.querySelector("#files").files; // 요청 파일
const formData = new FormData();
for(let file of files) {
formData.append("files", file);
}
formData.append("content",content);
postFileData(url,formData,csrf_header,csrf_token).then((response)=>{
console.log('response:',response)
})
Springboot Controller
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
@ResponseBody
public ResponseEntity<Object> questionCreate(
@RequestBody(required = false) List<MultipartFile> files,
@RequestBody @Valid QuestionForm questionForm,// @Valid 애노테이션을 통해 questionForm 의 @NotEmpty 등이 작동한다
BindingResult bindingresult,// @Valid 애노테이션으로 인해 검증된 결과를 의미하는 객체
Principal principal //현재 로그인한 사용자의 정보를 알려면 스프링 시큐리티가 제공하는 Principal 객체를 사용해야 한다.
){
//business logic 생략
return ResponseEntity.status(HttpStatus.OK).body(questionForm);
}
에러
the request was rejected because no multipart boundary was found
Fetch API의 Header부분 Content Type에 multipart/form 으로 직접 명시해서 요청을 보냈는데, 이렇게 하면 뒤에 boundary의 값이 없는 상태로 덮어씌워져 요청이 된다고 합니다.
Stackoverflow 답변을 보니 Content-Tpye에 multipart/form 으로 작성하지 말고 비워두고 진행하라고 합니다.
The problem is that you are setting theContent-Type
by yourself, let it be blank. Google Chrome will do it for you. The multipartContent-Type
needs to know the file boundary, and when you remove theContent-Type
, Postman will do it automagically for you.
FetchAPI 의 헤더 Content-Type을 주석처리하고 요청하니 Boundary의 값이 붙어서 잘 요청 되고 있습니다.
하지만 다른 에러가 발생했습니다
the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is application/json
SpringBoot Controller
Controller 부분의 @RequestBody 를 @RequestPart 로 변경해 주었습니다.
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
@ResponseBody
public ResponseEntity<Object> questionCreate(
@RequestPart(name = "files",required = false) List<MultipartFile> files,
@RequestPart(name = "content") @Valid QuestionForm questionForm,// @Valid 애노테이션을 통해 questionForm 의 @NotEmpty 등이 작동한다
BindingResult bindingresult,// @Valid 애노테이션으로 인해 검증된 결과를 의미하는 객체
Principal principal //현재 로그인한 사용자의 정보를 알려면 스프링 시큐리티가 제공하는 Principal 객체를 사용해야 한다.
){
return ResponseEntity.status(HttpStatus.OK).body(questionForm);
}
또 다른 에러 발생
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported]
Client의 Payload 를 확인해 보니, 본문 내용의 content-type이 application/octet-stream 으로 전송되고 있다.
Javascript
JS의 formData에 본문내용을 그대로 담지 말고 blob 객체의 타입을 application/json으로 만들어서 formdata에 append 해 주었습니다.
let postFileData = async (url, data = {}, csrf_header, csrf_token) => {
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE 등
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "multipart/form-data",
header: csrf_header,
'X-CSRF-Token': csrf_token
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: data
});
return response.json(); // JSON 응답을 네이티브 JavaScript 객체로 파싱
}
let content = form.querySelector(".question-input").value; // 요청 글
let files = document.querySelector("#files").files; // 요청 파일
const formData = new FormData();
for(let file of files) {
formData.append("files", file);
}
formData.append("content",new Blob([JSON.stringify(content)],{type: "application/json"}),
postFileData(url,formData,csrf_header,csrf_token).then((response)=>{
console.log('response:',response)
})
또 다른 에러 발생!!
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.weight.gym_dude.question.QuestionForm` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('안녕하세요 제가 쓰는 글 입니다')]
본문 내용을 deserialize 할 이름이 없다는 겁니다!!
JS 에서 Blob 객체를 만들때 {key:value} 형태로 만들지 않았었네요.
아래와 같이 변경해 주었습니다. 더 이상 에러를 발생하지 않고 잘 진행되었습니다.
//new Blob([JSON.stringify(content)]
new Blob([JSON.stringify({"content":content})]
최종 코드
Javascript
let postFileData = async (url, data = {}, csrf_header, csrf_token) => {
const response = await fetch(url, {
method: "POST", // *GET, POST, PUT, DELETE 등
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "multipart/form-data",
header: csrf_header,
'X-CSRF-Token': csrf_token
},
redirect: "follow",
referrerPolicy: "no-referrer",
body: data
});
return response.json(); // JSON 응답을 네이티브 JavaScript 객체로 파싱
}
let content = form.querySelector(".question-input").value; // 요청 글
let files = document.querySelector("#files").files; // 요청 파일
const formData = new FormData();
for(let file of files) {
formData.append("files", file);
}
formData.append("content",new Blob([JSON.stringify(content)],{type: "application/json"}),
postFileData(url,formData,csrf_header,csrf_token).then((response)=>{
console.log('response:',response)
})
SpringBoot Controller
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
@ResponseBody
public ResponseEntity<Object> questionCreate(
@RequestPart(name = "files",required = false) List<MultipartFile> files,
@RequestPart(name = "content") @Valid QuestionForm questionForm,// @Valid 애노테이션을 통해 questionForm 의 @NotEmpty 등이 작동한다
BindingResult bindingresult,// @Valid 애노테이션으로 인해 검증된 결과를 의미하는 객체
Principal principal //현재 로그인한 사용자의 정보를 알려면 스프링 시큐리티가 제공하는 Principal 객체를 사용해야 한다.
){
return ResponseEntity.status(HttpStatus.OK).body(questionForm);
}
참고 자료
https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/
https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch
https://somuchthings.tistory.com/160
https://velog.io/@hhhminme/Axios에서-Post-시-Contenttypeapplicationoctet-streamnotsupported-핸들링415-에러
댓글