mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
fix(merge): report a readable error when JPDFium natives cannot load
This commit is contained in:
@@ -581,6 +581,26 @@ public class ExceptionUtils {
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOException for a native library that could not be loaded.
|
||||
*
|
||||
* <p>Native loaders surface failures as {@link LinkageError} (typically {@link
|
||||
* ExceptionInInitializerError} on first use and {@link NoClassDefFoundError} afterwards). Those
|
||||
* are Errors, so they slip past every {@code catch (Exception)} guard and reach the caller as
|
||||
* an opaque failure with a null message.
|
||||
*
|
||||
* @param cause the linkage error raised by the native loader
|
||||
* @return IOException with user-friendly message
|
||||
*/
|
||||
public static IOException createNativeLibraryUnavailableException(LinkageError cause) {
|
||||
requireNonNull(cause, "cause");
|
||||
String message =
|
||||
getMessage(
|
||||
ErrorCode.NATIVE_LIBRARY_UNAVAILABLE.getMessageKey(),
|
||||
ErrorCode.NATIVE_LIBRARY_UNAVAILABLE.getDefaultMessage());
|
||||
return new IOException(message, cause);
|
||||
}
|
||||
|
||||
public static IOException createImageReadException(String filename) {
|
||||
requireNonNull(filename, "filename");
|
||||
String message =
|
||||
@@ -1218,6 +1238,10 @@ public class ExceptionUtils {
|
||||
|
||||
// System errors
|
||||
MD5_ALGORITHM("E080", "error.md5Algorithm", "MD5 algorithm not available"),
|
||||
NATIVE_LIBRARY_UNAVAILABLE(
|
||||
"E082",
|
||||
"error.nativeLibraryUnavailable",
|
||||
"A native library required for this operation could not be loaded on this system. This usually means the container image or platform is unsupported. Check the server logs for the underlying loader error."),
|
||||
OUT_OF_MEMORY_DPI(
|
||||
"E081",
|
||||
"error.outOfMemoryDpi",
|
||||
|
||||
@@ -282,6 +282,11 @@ public class MergeController {
|
||||
inputPaths.add(tempFile.toPath());
|
||||
|
||||
try (PdfDocument ignored = PdfDocument.open(tempFile.toPath())) {
|
||||
} catch (LinkageError e) {
|
||||
// Natives failed to load - not a problem with this file, so don't mark it
|
||||
// invalid and don't let the Error escape as an opaque 500.
|
||||
log.error("JPDFium native library unavailable", e);
|
||||
throw ExceptionUtils.createNativeLibraryUnavailableException(e);
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("PDF pre-validate", e);
|
||||
invalidIndexes.add(index);
|
||||
@@ -292,6 +297,9 @@ public class MergeController {
|
||||
try {
|
||||
pageCounts =
|
||||
mergeWithJpdfium(inputPaths, files, generateToc, mt.getFile().toPath());
|
||||
} catch (LinkageError e) {
|
||||
log.error("JPDFium native library unavailable", e);
|
||||
throw ExceptionUtils.createNativeLibraryUnavailableException(e);
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF merge", e);
|
||||
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
/**
|
||||
* Merging depends on JPDFium's native library. When those natives cannot be loaded the JVM raises
|
||||
* an ExceptionInInitializerError, which is an Error rather than an Exception - so every
|
||||
* catch(Exception) guard around PdfDocument.open() lets it through and the request fails with an
|
||||
* opaque 500 and a null message.
|
||||
*/
|
||||
class MergeControllerNativeLoadTest {
|
||||
|
||||
private MergeController mergeController;
|
||||
private TempFileManager tempFileManager;
|
||||
private byte[] pdfBytes;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
CustomPDFDocumentFactory factory = org.mockito.Mockito.mock(CustomPDFDocumentFactory.class);
|
||||
tempFileManager = org.mockito.Mockito.mock(TempFileManager.class);
|
||||
when(tempFileManager.createTempFile(any()))
|
||||
.thenAnswer(inv -> Files.createTempFile("merge-native", ".pdf").toFile());
|
||||
when(tempFileManager.convertMultipartFileToFile(any(MultipartFile.class)))
|
||||
.thenAnswer(
|
||||
inv -> {
|
||||
MultipartFile mf = inv.getArgument(0);
|
||||
Path p = Files.createTempFile("merge-native-in", ".pdf");
|
||||
Files.write(p, mf.getBytes());
|
||||
return p.toFile();
|
||||
});
|
||||
mergeController = new MergeController(factory, tempFileManager);
|
||||
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
doc.addPage(new PDPage());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
pdfBytes = baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reports a readable error when the JPDFium natives cannot be loaded")
|
||||
void reportsReadableErrorWhenNativesUnavailable() throws Exception {
|
||||
MergePdfsRequest request = new MergePdfsRequest();
|
||||
request.setFileInput(
|
||||
new MultipartFile[] {
|
||||
new MockMultipartFile(
|
||||
"fileInput", "a.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes),
|
||||
new MockMultipartFile(
|
||||
"fileInput", "b.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes)
|
||||
});
|
||||
|
||||
try (MockedStatic<PdfDocument> natives = mockStatic(PdfDocument.class)) {
|
||||
natives.when(() -> PdfDocument.open(any(Path.class)))
|
||||
.thenThrow(
|
||||
new ExceptionInInitializerError(
|
||||
new RuntimeException("Failed to load native library")));
|
||||
|
||||
Throwable thrown =
|
||||
assertThrows(Throwable.class, () -> mergeController.mergePdfs(request, null));
|
||||
|
||||
assertInstanceOf(
|
||||
IOException.class,
|
||||
thrown,
|
||||
"a native-load failure must surface as a handled IOException, not a raw Error");
|
||||
assertTrue(
|
||||
thrown.getMessage() != null && !thrown.getMessage().isBlank(),
|
||||
"the failure must carry a message the caller can act on");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Cleans up the temp output when the natives cannot be loaded")
|
||||
void doesNotLeakTempFilesWhenNativesUnavailable() throws Exception {
|
||||
MergePdfsRequest request = new MergePdfsRequest();
|
||||
request.setFileInput(
|
||||
new MultipartFile[] {
|
||||
new MockMultipartFile(
|
||||
"fileInput", "a.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes)
|
||||
});
|
||||
|
||||
try (MockedStatic<PdfDocument> natives = mockStatic(PdfDocument.class)) {
|
||||
natives.when(() -> PdfDocument.open(any(Path.class)))
|
||||
.thenThrow(
|
||||
new ExceptionInInitializerError(
|
||||
new RuntimeException("Failed to load native library")));
|
||||
|
||||
assertThrows(IOException.class, () -> mergeController.mergePdfs(request, null));
|
||||
}
|
||||
|
||||
org.mockito.Mockito.verify(tempFileManager, org.mockito.Mockito.atLeastOnce())
|
||||
.deleteTempFile(any(File.class));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user