@senspond

>

개발>프론트 엔드

Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법, 수동전송 방법(JAVA)

등록일시 : 2024-02-23 (금) 02:46
업데이트 : 2024-02-23 (금) 02:54
오늘 조회수 : 8
총 조회수 : 2277

    Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법을 정리하고 multipart/formdata 수동전송 방법을 정리한 내용입니다.

    form 파일 업로드

    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 사용시)

    하지만 아래처럼 사용하게 되면 문제가 발생 할 수가 있다.


    - 클라이언트: 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 가 들어간것을 볼 수가 있다.


    mutipart/form-data 수동으로 구성하기

    웹 개발을 하다보면 간혹 Server to Server 서버에서 다른 서버로 파일을 전송해야 하는 경우가 있다. 그런 경우 프로토콜에 맞게 작성해서 보내줘야 한다. 예를 들자면, nodejs 백엔드 서버에서 Spring 백엔드 서버로 파일을 전송 하는 경우 등이 있을 수 있다.


    HTTP요청의 multipart/formdata를 수동으로 구성하기 위해서는

    1. 헤더에 Content-type=multipart/formdata으로 콘텐츠타입을 주고, 이 때 각 파트간의 경계를 위한 구분자를 추가한다.. Content-type=multipart/formdata; boundary=--....

    2. 구분자는 반드시 --으로 시작해야 한다.

    3. 각각의 파트에는 헤더가 들어가며, Content-Disposition: form-data; name=" ... " 과 같은 폼 필드의 정보를 추가합한다.

    4. 모든 파트가 끝나면 구분자 뒤에 --을 추가해 준다.


    예시

    예시는 자바 서버에서 다른 외부 서버로 파일전송 코드이다.


    예전에 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 를 공부 중인데... 프론트엔드에서 백엔드의 전유물로만 생각되었던 머신러닝을 해버리니 묘하더라고요. 그것도 마찬가지인것 같습니다 ㅎㅎ






    senspond

    안녕하세요. Red, Green, Blue 가 만나 새로운 세상을 만들어 나가겠다는 이상을 가진 개발자의 개인공간입니다.

    댓글 ( 0 )

    카테고리내 관련 게시글

    현재글에서 작성자가 발행한 같은 카테고리내 이전, 다음 글들을 보여줍니다

    @senspond

    >

    개발>프론트 엔드

    • NextJs 에서 동적으로 Sitemap 만드는 방법

      이번 글에는 React 기반으로 만들어진 NextJs 프레임워크에서 동적으로 Sitemap 만드는 방법에 대해서 정리해보고 현재 발견한 문제점에 대해서 적어보려고 합니다.
        2024-01-03 (수) 05:33
      1. 타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기

        타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기 정리를 해봤습니다.
          2024-05-22 (수) 07:59
        1. NextJS App router 방식으로 다국어 설정 간단하게 하기 next-intl

          NextJS App router 방식으로 다국어 설정 하기 next-intl 를 통해 간단하게 적용
            2024-09-18 (수) 03:38
          1. [현재글] Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법, 수동전송 방법(JAVA)

            Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법을 정리하고 multipart/formdata 수동전송 방법을 정리한 내용입니다.
              2024-02-23 (금) 02:46
            1. NextJs 프로젝트에 Jest 테스트 프레임워크 적용하기

              Jest는 메타(구 페이스북)가 유지보수하는 재스민(jasmine) 위에서 빌드되는 테스팅 프레임워크입니다. 이 글은 NextJs 프로젝트에 Jset 프레임워크를 적용하는 방법을 정리해봅니다.
                2024-01-19 (금) 11:32