Файл изображения поврежден только при загрузке из Mobile Safari в Spring Boot на AWS EC2.IOS

Программируем под IOS
Ответить
Anonymous
 Файл изображения поврежден только при загрузке из Mobile Safari в Spring Boot на AWS EC2.

Сообщение Anonymous »

Я столкнулся с очень странной и постоянной проблемой с загрузкой изображений, которая возникает только в Mobile-Safari и только тогда, когда сервер работает на AWS EC2.
Я потратил значительное количество времени на ее отладку и на данный момент пытаюсь определить, является ли это проблемой взаимодействия Safari + сети/серверной среды, а не ошибкой на уровне приложения.
Загрузка изображений работает отлично работает:
Chrome / Firefox (все платформы)
Safari, когда сервер работает на моем локальном ноутбуке с Windows 11
Не удается загрузить изображение (поврежденное изображение) на:
Safari (macOS / iOS)
Когда сервер развернут на AWS EC2

Изображение уже повреждено перед любой обработкой, непосредственно на уровне контроллера Spring Boot
Повреждение проявляется в следующем: Случайные горизонтальные линии. Неправильная структура JPEG.
Разное шестнадцатеричное/двоичное содержимое по сравнению с исходным файлом.
Что я пробовал
  • Протестировано несколько методов загрузки
    multipart/form-data
    application/octet-stream
    XMLHttpRequest
    Отправка необработанного объекта File без переноса
    Нет ручного заголовка Content-Type
    Нет манипуляции с именем файла
→ Тот же результат: Safari + EC2 = поврежденное изображение
Спецификации сервера
  • Протестированные экземпляры EC2: t2.micro, t3.xlarge
    Одинаковое поведение независимо от процессора/памяти
*** Я не пробовал это в Safari на рабочем столе. Только Mobile-Safari**
Изображение

Изображение
Изображение

@PostMapping(value = "/api/ckeditor/imageUpload", потребляет = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE} )
@ResponseBody
public Map saveContentsImage(@RequestPart(value = "upload", требуется = true) MultipartFile uploadFile, HttpServletRequest req, HttpServletResponse httpsResponse) {
// ────────────────────────────────
// 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 ДанныеФормы();
// *** 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}%)`;
}
}
};


Подробнее здесь: https://stackoverflow.com/questions/798 ... oot-on-aws
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «IOS»