Backend · Infra

ajax로 다운로드가 안될 때 : Uncaught InvalidStateError: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was 'blob').

devhyen 2024. 10. 22. 14:13
👑 목차 🎀
    반응형

     

    파일 다운로드를 하는 방법은 여러가지가 있는데, 

    가장 간단하게는 

    location.href = 'api 주소';

     

    로 구현할 수 있다. 

     

    제이쿼리를 쓰고 있는 프로젝트였고, 파일의 크기가 클 수 있기 때문에 

    사용자들이 기다리는 것을 위해서 spinner 를 다운로드 받는 중에 노출 시키도록 변경 하려고 했다. 

    단순히 링크만 보내는 것은 파일이 다운로드가 완료 된 후에 spinner를 끄는 것이 불가하기 때문에 

    ajax를 사용하기로 한다. 

     

    일반적으로 Ajax 요청은 JSON,XML,HTML과 같은 텍스트 데이터를 처리하지만, 파일 다운로드의 경우 바이너리데이터(blob)으로 응답이 전달된다. 

    그렇기 때문에 

    xhrFields: {
        responseType: 'blob'
    }

     

    와 같은 설정이 필요하다. 

     

    $.ajax({
                        url: /download?${filePaths},
                        method: 'GET',
                        xhrFields: {
                            responseType: 'blob',
                            withCredentials: true  // 쿠키를 포함하여 요청
                        },
                        success: function(data, status, xhr) {
                            // 파일 이름 추출 (백엔드의 Content-Disposition 헤더 사용)
                            var filename = "";
                            var disposition = xhr.getResponseHeader('Content-Disposition');
                            if (disposition && disposition.indexOf('attachment') !== -1) {
                                var matches = /filename="([^"]*)"/.exec(disposition);
                                if (matches != null && matches[1]) {
                                    filename = matches[1];
                                }
                            }
    
                            // Blob을 사용해 파일 다운로드
                            var blob = data;  // responseType이 blob일 때, data가 이미 blob으로 전달됨
                            var link = document.createElement('a');
                            link.href = window.URL.createObjectURL(blob);
                            link.download = filename || 'download.zip';  // 기본 파일 이름 설정
                            link.click();
                        },
                        error: function() {
                            alert("File download failed.");
                        }
                    });
    
                },
                error: function(xhr, status, error) {
                    console.error('Error:', error);
                }
            })
        });

     

    위와 같은 코드를 작성하였는데, 

     

    Uncaught InvalidStateError: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was 'blob'). at XMLHttpRequest.c (jquery.min:3:26787)

     

    에러발생..

     

    찾아보니 jquery 버전의 문제였다. 

    jquery 1.12.1 버전을 3.3.1버전으로 변경하게 되면 문제가 발생하지 않는다고 한다. 

     

    하지만, 나 혼자 하는 프로젝트가 아니다 보니 

    jquery 버전을 올릴 수는 없고, 

    fetch방식으로 변경하게 되었다. 

    fetch('/download')
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.blob(); // Blob으로 응답을 처리
        })
        .then(blob => {
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'yourfile.zip'; // 원하는 파일 이름 지정
            document.body.appendChild(a);
            a.click();
            a.remove();
        })
        .catch(error => console.error('There has been a problem with your fetch operation:', error));

    fetch 방식으로 변경하다 보니 그래도 에러가 계속 발생함 

    디버깅을 해보니, 서버측에서 발생하는 오류로 

    Could not write content: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.core.io.InputStreamResource["inputStream"]->java.io.FileInputStream["fd"])

     

    이 오류는 Spring이 InputStreamResource에서 InputStream을 직렬화 하려고 시도하면서 발생한 오류이다. 

    Spring이 직렬화할 수 없는 객체를 포함하고 있어서 나타나는 문제다..

     

    href.location 방식에서는 나타나지 않던 문제가 fetch API에서는 발생한건가? 

    • fetch를 사용하는 경우 자바스크립트로 HTTP 요청을 보내고, 서버로부터 받은 응답을 수동으로 처리해야한다.
    • fetch는 JSON응답을 처리하기 위해 설계된 API로 기본적으로 JSON으로 변환하려고 시도한다.
    • 이 과정에서 InputStreamResource 같은 스트리밍 데이터를 처리할 때 문제가 발생할 수 있다.

    반면, href를 동해 하게 된다면, 응답에 Content-Disposition 헤더가 포함되어 있어 브라우저가 파일을 다운로드를 처리할 수 있다. 

     

    수정된 서버 코드 

    public void downloadZip(HttpServletResponse response) {
            Path zipFilePath;
    
            try {
                // Zip 파일 생성
                zipFilePath = fileService.createZipFile("filepath");
    
                // 파일이 존재하는지 확인
                if (!Files.exists(zipFilePath)) {
                    response.sendError(HttpServletResponse.SC_NOT_FOUND, "Zip file not found: " + zipFilePath.toString());
                    return;
                }
    
                File zipFile = zipFilePath.toFile();
                try (FileInputStream fileInputStream = new FileInputStream(zipFile);
                     OutputStream outputStream = response.getOutputStream()) {
    
                    // 응답 헤더 설정
                    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + new String(zipFile.getName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1) + "\"");
                    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    
                    // 파일 내용을 스트리밍
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = fileInputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                    outputStream.flush();
                }
            } catch (IOException e) {
                // IO 예외 처리
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to download zip file: " + e.getMessage());
            } catch (Exception e) {
                // 일반 예외 처리
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "An unexpected error occurred: " + e.getMessage());
            }
        }
    }

     

    클라이언트로 파일 전송을 위해 HttpServletResponse를 사용하여 응답을 직접 설정했다. 

    원래 문제가 발생한 이유 

    • InputStreamResource로 zip 파일을 응답으로 반환할 때, Spring이 JSON으로 직렬화 하려고 시도함
    • InputStreamResource내부에서 사용되는 FileInputStream은 직렬화 할 수 없는 클래스
    • 그 과정에서 오류가 발생

    수정된 원리

    • HttpServletResponse를 사용하여 직접 응답을 구성하여 Spring의 JSON 직렬화 하는 것을 우회할 수 있다.
    • 응답을 직접 설정하므로 파일을 스트리밍 할 수 있고, 이 과정에서 Spring은 JSON으로 직렬화 하려고 시도하지 않는다. 
    • FileInputStream을 사용하여 Zip파일 내용을 읽고, OutputStream을 통해 클라이언트에 직접 전송한다. 메모리 사용이 효율적이다.
    • try-with-resources 를 사용하여 자동으로 자원을 닫아 메모리 누수를 방지한다. 

     

     

    반응형