Post

2024-09-25-TIL

2024-09-25-TIL

Today I Learned

Filedownload Troubleshooting

MP3 음원은 브라우저가 기본적으로 제공하는 플레이어를 통해서 다운로드 받을 수 있지만, 이는 플레이어를 통해서 간접적으로 다운로드 받는것이라 정확히 원본이라고 할 수는 없다. 따라서 직접 음원 다운로드 API를 작성해서 핸들링하게 되었다.

이때 파일명과 확장자를 임의로 지정하도록 강제하기 위해서 Content-Disposition 헤더를 사용하고, 인코딩 방식을 지정하기 위해서 Content-Transfer-Encoding 헤더를 사용했다. 이 두개의 헤더에 대해서 Frontend인 Vue.js에서 접근하지 못하는 상황이라서 해당 API에 한해서만 별도의 Cross Origin 설정을 통해 특정 헤더를 노출시키도록 설정해두었다. 직접 헤더에 추가해도 되지만 스프링에서 제공하는 @CrossOrigin을 사용하면 속성으로 노출할 헤더를 지정할 수 있으며, 이를 허용할 cross-origins를 직접 지정할수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  @CrossOrigin(
      origins = "*",
      exposedHeaders = {HttpHeaders.CONTENT_DISPOSITION, "Content-Transfer-Encoding"})
  @GetMapping("/download")
  public ResponseEntity<Resource> download(
      @PathVariable Long trackId, @RequestParam String streamUrl) {
    URI uri = new URI(streamUrl);
    HttpResponse<InputStream> response;
    try (HttpClient client =
        HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build()) {
      HttpRequest request =
          HttpRequest.newBuilder().uri(uri).timeout(Duration.ofMinutes(1)).build();
      response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
      if (response.statusCode() != 200) {
        return ResponseEntity.status(response.statusCode()).build();
      }

      byte[] content;
      try (InputStream inputStream = response.body()) {
        content = inputStream.readAllBytes();
      }
      ByteArrayResource resource = new ByteArrayResource(content);

      HttpHeaders headers = new HttpHeaders();
      headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + trackId + ".mp3\";");
      headers.set("Content-Transfer-Encoding", "binary");

      return ResponseEntity.ok().headers(headers).body(resource);
    }
  }

그런데 API를 통해서 다운로드 받으면 6.5MB에 test.mp3로 정상적인 파일이 받아지는데, 브라우저에서 Frontend 애플리케이션을 통해서 받으면 계속 11.5MB로 부풀려지면서 음원의 재생도 불가능한 이슈가 있었다. 두 개의 파일은 분명히 차이가 있었고 깨진 파일은 duration도 인식하지 못하고 있었다. 바이너리의 헥사값을 비교한 첫부분은 다음과 같다.

정상파일:

1
FFFBE444 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 496E666F 0000000F 00001BDA 00687540 00030508 0A0D1012 14161A1C 1F212427 292B2D31 3336383B 3E404244 484A4C4F 52555759 5B5F6163 66696C6E 70727678 7A7D8082 8587898D 8F919496 999C9EA0 A4A6A8AB ADB0B3B5 B7BBBDBF C2C4C7CA CCCED2D4 D6D9DBDE E1E3E5E9 EBEDF0F2 F5F8FAFC 00000039 4C414D45 332E3130 3001CD00 0000002E 5E000034 FF240261 8D000140 00687540 B72530D8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

깨진파일:

1
EFBFBDEF BFBDEFBF BD440000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000496E 666F0000 000F0000 1BEFBFBD 00687540 00030508 0A0D1012 14161A1C 1F212427 292B2D31 3336383B 3E404244 484A4C4F 52555759 5B5F6163 66696C6E 70727678 7A7DEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD EFBFBDEF BFBDEFBF BDEFBFBD 00000039 4C414D45 332E3130 3001EFBF BD000000 002E5E00 0034EFBF BD240261 EFBFBD00 01400068 7540EFBF BD2530EF BFBD0000 00000000 00000000

깨진 파일의 첫 부분에 EFBFBDEF라는 값이 눈에 띄는데, 이는 raw 바이너리 파일을 UTF-8로 잘못 해석하려고 할 때 붙게 되는 값이라고 한다.

The difference in the binary files you’ve provided suggests that the second file (with EFBFBDEF BFBDEFBF BD) contains repeated occurrences of the byte sequence EFBFBDEF, which is characteristic of UTF-8 encoding for replacement characters (�). This typically appears when a binary file is incorrectly interpreted as text, and non-text binary sequences are replaced with the � character.

In this case, it appears the second file may have been read or handled as a UTF-8 string rather than a raw binary file, leading to corruption. The EFBFBDEF sequence replaces parts of the original binary data, which explains why the file is unreadable or fails to play properly.

To summarize:

The first file (starting with FFFBE444) seems to have the correct binary structure for an MP3 file. The second file (with EFBFBDEF sequences) appears to have been misinterpreted as text (likely UTF-8), which led to data corruption, rendering the file unusable. This further supports the hypothesis that the file was incorrectly handled as a text-based format before you added format: ‘blob’ to the Vue.js code. By specifying blob, you ensured that the binary data was treated correctly, preserving the file’s integrity.

결국 원인은 Frontend에서 blob이 아닌 JSON이나 텍스트로 취급해서 UTF-8로 해석하려고 하면서 파일이 깨진것으로 보인다. 다음의 수정 사항으로 해소되었다.

1
2
3
4
5
6
7
8
9
vue
      http.httpClient.tracks.download
      (
        {
          trackId: body.trackId,
          streamUrl: body.streamUrl
        },
        { format: 'blob' } <- 추가
      ),

MIME

MIME는 Multipurpose Internet Mail Extensions의 줄임말로, ASCII 가 아닌 character set의 텍스트와 오디오, 비디오, 이미지 및 애플리케이션 프로그램의 첨부 파일을 지원하도록 이메일 메시지의 형식을 확장하는 표준이다. 메시지 본문은 여러 부분으로 구성될 수 있으며 헤더 정보는 ASCII가 아닌 character set으로 지정할 수 있다. MIME 형식의 이메일 메시지는 일반적으로 SMTP, POP, IMAP과 같은 표준 프로토콜을 사용하여 전송된다.

MIME 형식주의는 주로 SMTP를 위해 설계되었지만, 그 콘텐츠 유형은 다른 통신 프로토콜 에서도 중요하다 . World Wide Web의 HyperText Transfer Protocol(HTTP)에서 서버는 모든 웹 전송의 시작 부분에 MIME 헤더 필드를 삽입한다. 클라이언트는 콘텐츠 유형 또는 미디어 유형 헤더를 사용하여 표시된 데이터 유형에 적합한 뷰어 애플리케이션을 선택한다.

A media type (also known as a Multipurpose Internet Mail Extensions or MIME type) indicates the nature and format of a document, file, or assortment of bytes. MIME types are defined and standardized in IETF’s RFC 6838. {: .prompt-danger }

Warning: Browsers use the MIME type, not the file extension, to determine how to process a URL, so it’s important that web servers send the correct MIME type in the response’s Content-Type header. If this is not correctly configured, browsers are likely to misinterpret the contents of files, sites will not work correctly, and downloaded files may be mishandled. {: .prompt-danger }

Today I Read

This post is licensed under CC BY 4.0 by the author.