Die Bilddatei ist nur beschädigt, wenn sie von Mobile Safari auf Spring Boot auf AWS EC2 hochgeladen wirdIOS

Programmierung für iOS
Anonymous
 Die Bilddatei ist nur beschädigt, wenn sie von Mobile Safari auf Spring Boot auf AWS EC2 hochgeladen wird

Post by Anonymous »

Ich stehe vor einem sehr seltsamen und anhaltenden Problem beim Hochladen von Bildern, das nur in Mobile-Safari und nur dann auftritt, wenn der Server auf AWS EC2 läuft.
Ich habe viel Zeit damit verbracht, dieses Problem zu beheben, und an dieser Stelle versuche ich festzustellen, ob es sich hierbei um ein Interaktionsproblem zwischen Safari + Netzwerk-/Serverumgebung und nicht um einen Fehler auf Anwendungsebene handelt.
Das Hochladen von Bildern funktioniert einwandfrei auf:
Chrome / Firefox (alle Plattformen)
Safari, wenn der Server auf meinem lokalen Windows 11-Laptop läuft
Bild-Upload schlägt fehl (beschädigtes Bild) auf:
Safari (macOS / iOS)
Wenn der Server auf AWS EC2 bereitgestellt wird

Das Bild ist bereits vor der Verarbeitung beschädigt, direkt am Spring Boot-Controller-Ebene
Die Beschädigung ist sichtbar als: Zufällige horizontale Linien Defekte JPEG-Struktur
Anderer Hex-/Binärinhalt im Vergleich zur Originaldatei
Was ich versucht habe
  • Mehrere getestete Upload-Methoden
    multipart/form-data
    application/octet-stream
    XMLHttpRequest
    Rohdateiobjekt ohne Umbruch senden
    Kein manueller Content-Type-Header
    Keine Dateinamenmanipulation
→ Gleiches Ergebnis: Safari + EC2 = beschädigtes Bild
Serverspezifikationen
  • Getestet mit EC2 Instanzen: t2.micro, t3.xlarge
    Gleiches Verhalten unabhängig von CPU/Speicher
*** Ich habe es nicht auf der Safari auf dem Desktop ausprobiert. Nur Mobile-Safari**
Image

Image
Image

@PostMapping(value = "/api/ckeditor/imageUpload", verbraucht = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE} )
@ResponseBody
public Map saveContentsImage(@RequestPart(value = "upload", erforderlich = true) MultipartFile uploadFile, HttpServletRequest req, HttpServletResponse httpsResponse) {

Code: Select all

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

log.info("=== [UPLOAD DEBUG] CKEditor Image Upload Request ===");
log.info("Request Method       : {}", req.getMethod());
log.info("Client IP            : {}", req.getRemoteAddr());
log.info("User-Agent           : {}", userAgent);
log.info("Detected Browser     : {}", detectBrowser(userAgent));
log.info("Content-Type (Part)  : {}", contentType);
log.info("Content-Length (Hdr) : {}", req.getHeader("Content-Length"));
log.info("Multipart Size       : {} bytes", fileSize);
log.info("=====================================================");

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

// ================== [DEBUG] Save raw file at Controller level (User's requested method) ==================
try {
String projectRootPath = System.getProperty("user.dir");
java.io.File debugDir = new java.io.File(projectRootPath, "tmp_debug");

if (!debugDir.exists()) {
debugDir.mkdirs();
}
// Sanitize the original filename to prevent path traversal issues
String originalFilename = org.springframework.util.StringUtils.cleanPath(uploadFile.getOriginalFilename());
// Differentiate the filename to indicate it's from the controller
java.io.File rawDebugFile = new java.io.File(debugDir, "controller_raw_" + originalFilename);

log.info("CONTROLLER DEBUG: Attempting to copy uploaded file to: {}", rawDebugFile.getAbsolutePath());

// Use InputStream to copy the file, which does not move the original temp file.
try (java.io.InputStream in = uploadFile.getInputStream();
java.io.OutputStream out = new java.io.FileOutputStream(rawDebugFile)) {
in.transferTo(out);
}
log.info("CONTROLLER DEBUG: File successfully copied.  Size: {} bytes", rawDebugFile.length());

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

Long sessionCustomerId = SessionUtils.getSessionCustomerId(req);
if (sessionCustomerId == null) {
Map 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 response = new HashMap();
response.put("uploaded", 1);
response.put("fileName", uploadFile.getOriginalFilename());
response.put("url", imageUrl);
return response;
}
=========================================================================
JS CODE
const data = new FormData();

Code: Select all

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

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

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

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

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

//const cleanBlob = new Blob([originalFile], { type: originalFile.type });
//data.set('upload', originalFile, toAsciiFilename(originalFile.name));

//data.set('upload', originalFile,toAsciiFilename(originalFile.name));
//data.append("upload", safeFile);

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

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

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post