17.5 C
Canberra
Sunday, January 18, 2026

Picture file corrupted solely when uploaded from Cell Safari to Spring Boot on AWS EC2


I’m going through a really unusual and protracted picture add situation that happens solely in Cell-Safari, and solely when the server is operating on AWS EC2.

I’ve spent a major period of time debugging this, and at this level I’m attempting to find out whether or not this can be a Safari + community/server surroundings interplay situation, moderately than an application-level bug.

Picture add works completely on:
Chrome / Firefox (all platforms)
Safari when the server is operating on my native Home windows 11 laptop computer

Picture add fails (corrupted picture) on:
Safari (macOS / iOS)
When the server is deployed on AWS EC2

The picture is already corrupted earlier than any processing, instantly on the Spring Boot controller stage

The corruption is seen as: Random horizontal traces Damaged JPEG construction

Totally different hex/binary content material in comparison with the unique file

What I attempted

  • A number of add strategies examined

    multipart/form-data

    software/octet-stream

    XMLHttpRequest

    Sending uncooked File object with out wrapping

    No handbook Content material-Sort header

    No filename manipulation

→ Similar outcome: Safari + EC2 = corrupted picture

Server specs

  • Examined EC2 situations: t2.micro, t3.xlarge

    Similar habits no matter CPU/reminiscence

*** I did not strive it on the safari on the desktop. Solely Cell-Safari**

Picture file corrupted solely when uploaded from Cell Safari to Spring Boot on AWS EC2

normal hex dump
corrupted hex dump

@PostMapping(worth = "/api/ckeditor/imageUpload", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE} )
@ResponseBody
public Map<String, Object> saveContentsImage(@RequestPart(worth = "add", required = true) MultipartFile uploadFile, HttpServletRequest req, HttpServletResponse httpsResponse) {

    // ────────────────────────────────
    // 1️⃣ 기본 요청 정보 로깅
    // ────────────────────────────────
    String userAgent = req.getHeader("Consumer-Agent");
    String contentType = uploadFile.getContentType();
    lengthy fileSize = uploadFile.getSize();

    log.data("=== [UPLOAD DEBUG] CKEditor Picture Add Request ===");
    log.data("Request Technique       : {}", req.getMethod());
    log.data("Consumer IP            : {}", req.getRemoteAddr());
    log.data("Consumer-Agent           : {}", userAgent);
    log.data("Detected Browser     : {}", detectBrowser(userAgent));
    log.data("Content material-Sort (Half)  : {}", contentType);
    log.data("Content material-Size (Hdr) : {}", req.getHeader("Content material-Size"));
    log.data("Multipart Measurement       : {} bytes", fileSize);
    log.data("=====================================================");

    // Safari 업로드 이슈 발생 빈도가 높으므로 별도 디버그 경로에 저장
    boolean isSafari = userAgent != null && userAgent.comprises("Safari") && !userAgent.comprises("Chrome");
    

    // ================== [DEBUG] Save uncooked file at Controller stage (Consumer's requested methodology) ==================
    strive {
        String projectRootPath = System.getProperty("consumer.dir");
        java.io.File debugDir = new java.io.File(projectRootPath, "tmp_debug");

        if (!debugDir.exists()) {
            debugDir.mkdirs();
        }
        // Sanitize the unique filename to forestall path traversal points
        String originalFilename = org.springframework.util.StringUtils.cleanPath(uploadFile.getOriginalFilename());
        // Differentiate the filename to point it is from the controller
        java.io.File rawDebugFile = new java.io.File(debugDir, "controller_raw_" + originalFilename);

        log.data("CONTROLLER DEBUG: Trying to repeat uploaded file to: {}", rawDebugFile.getAbsolutePath());

        // Use InputStream to repeat the file, which doesn't transfer the unique temp file.
        strive (java.io.InputStream in = uploadFile.getInputStream();
             java.io.OutputStream out = new java.io.FileOutputStream(rawDebugFile)) {
            in.transferTo(out);
        }
        log.data("CONTROLLER DEBUG: File efficiently copied. Measurement: {} bytes", rawDebugFile.size());

    } catch (Exception e) {
        log.error("CONTROLLER DEBUG: Failed to repeat debug file.", e);
    }
    // ================== [DEBUG] END ===================

    Lengthy sessionCustomerId = SessionUtils.getSessionCustomerId(req);
    if (sessionCustomerId == null) {
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("uploaded", 0);
        errorResponse.put("error", Map.of("message", "세션이 만료되었거나 로그인 상태가 아닙니다."));
        return errorResponse;
    }

    String customerId = sessionCustomerId.toString();
    String imageUrl = postUtils.saveContentsImage(uploadFile, customerId);
    Map<String, Object> response = new HashMap<>();
    response.put("uploaded", 1);
    response.put("fileName", uploadFile.getOriginalFilename());
    response.put("url", imageUrl);
    return response;
}

=============================================================================

JS CODE

const information = new FormData();

        // *** CSRF 토큰을 FormData에 직접 추가 ***
        const csrfToken = doc.querySelector('meta[name="_csrf"]')?.getAttribute('content material');
        const csrfParameterName = doc.querySelector('meta[name="_csrf_parameter"]')?.getAttribute('content material') || '_csrf';

        if (csrfToken) {
            console.log(csrfToken);
            information.append(csrfParameterName, csrfToken);
        }

       // const safeFile = new File([finalBlob], finalFileName, { sort: finalMimeType });
        //const safeFile = new File([ab], finalFileName, { sort: finalMimeType });


        // ✅ 2) File() 감싸지 말고 그대로 append
        //information.append('add', finalBlob, finalFileName);



        // 원본 File 그대로 append (Blob 래핑 ❌)
        //information.append('add', originalFile);
        // 원본 파일의 내용을 기반으로 Content material-Type이 'software/octet-stream'으로 지정된 새 File 객체를 생성합니다.
        const octetStreamFile = new File([finalBlob], finalFileName, {
            sort: 'software/octet-stream'
        });
        console.log(`[UploadAdapter] Content material-Type을 'software/octet-stream'으로 강제 변환하여 업로드를 시도합니다.`
        );
        // 새로 생성된 File 객체를 FormData에 추가합니다.
        information.append('add', octetStreamFile);

        //const cleanBlob = new Blob([originalFile], { sort: originalFile.sort });
        //information.set('add', originalFile, toAsciiFilename(originalFile.title));

        //information.set('add', originalFile,toAsciiFilename(originalFile.title));
        //information.append("add", safeFile);




        // === fetch 업로드 (대체)  시작 ===
        const xhr = new XMLHttpRequest();
        xhr.open("POST", "/api/ckeditor/imageUpload", true);
        xhr.withCredentials = true;

        // Safari의 multipart/form-data 업로드 안정성을 높이기 위해 수동 boundary 지정 없이 자동 생성하게 둠
        // Content material-Type을 직접 지정하지 않음 — 브라우저가 자동으로 생성하도록
        xhr.add.onprogress = (occasion) => {
            if (occasion.lengthComputable) {
                const percentCompleted = Math.spherical((occasion.loaded * 100) / occasion.complete);
                const progressBarFill = doc.querySelector('.progressBarFill');
                if (progressBarFill) {
                    progressBarFill.type.width = percentCompleted + '%';
                }
                const progressText = doc.querySelector('.uploadingText');
                if (progressText) {
                    progressText.textContent = `이미지 업로드 중... ${Math.spherical(occasion.loaded / 1024)}KB / ${Math.spherical(occasion.complete / 1024)}KB (${percentCompleted}%)`;
                }
            }
        };

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles