fix(api): return 204 No Content on failed PDF filters; add OpenAPI responses and safe resource handling (#4406)

# Description of Changes

- **What was changed**
- Added explicit `@ApiResponses` for filter endpoints with `200` (PDF
passed) and `204` (did not pass) including `@Content` and media types.
- Replaced ambiguous `null` returns with
`ResponseEntity.noContent().build()` when a filter condition is not met.
- Ensured `PDDocument` is properly closed using try-with-resources in
relevant endpoints.
- Consolidated comparison logic into a reusable, type-safe `compare<T
extends Comparable<T>>()` helper for page count, page size, file size,
and rotation checks.
  - Minor cleanup and consistency improvements across filter endpoints.

- **Why the change was made**
- To return correct HTTP semantics (avoid ambiguous `null` responses)
and improve API reliability for clients consuming these endpoints.
- To document expected responses clearly in the OpenAPI spec for better
consumer tooling and DX.
- To prevent potential resource leaks by consistently closing
`PDDocument`.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Ludy 2025-10-29 22:54:23 +01:00 committed by GitHub
parent 5e281fa002
commit f48f80927a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -15,6 +15,9 @@ import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -42,6 +45,16 @@ public class FilterController {
@Operation( @Operation(
summary = "Checks if a PDF contains set text, returns true if does", summary = "Checks if a PDF contains set text, returns true if does",
description = "Input:PDF Output:Boolean Type:SISO") description = "Input:PDF Output:Boolean Type:SISO")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "PDF passed filter",
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
@ApiResponse(
responseCode = "204",
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
@ -54,148 +67,184 @@ public class FilterController {
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
} }
} }
return null; return ResponseEntity.noContent().build();
} }
// TODO
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
@Operation( @Operation(
summary = "Checks if a PDF contains an image", summary = "Checks if a PDF contains an image",
description = "Input:PDF Output:Boolean Type:SISO") description = "Input:PDF Output:Boolean Type:SISO")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "PDF passed filter",
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
@ApiResponse(
responseCode = "204",
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request) public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String pageNumber = request.getPageNumbers(); String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = pdfDocumentFactory.load(inputFile); try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) {
if (PdfUtils.hasImages(pdfDocument, pageNumber)) if (PdfUtils.hasImages(pdfDocument, pageNumber)) {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
return null; }
}
return ResponseEntity.noContent().build();
} }
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
@Operation( @Operation(
summary = "Checks if a PDF is greater, less or equal to a setPageCount", summary = "Checks if a PDF is greater, less or equal to a setPageCount",
description = "Input:PDF Output:Boolean Type:SISO") description = "Input:PDF Output:Boolean Type:SISO")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "PDF passed filter",
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
@ApiResponse(
responseCode = "204",
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request) public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
int pageCount = request.getPageCount(); int pageCount = request.getPageCount();
String comparator = request.getComparator(); String comparator = request.getComparator();
// Load the PDF
PDDocument document = pdfDocumentFactory.load(inputFile);
int actualPageCount = document.getNumberOfPages();
// Perform the comparison
boolean valid =
switch (comparator) {
case "Greater" -> actualPageCount > pageCount;
case "Equal" -> actualPageCount == pageCount;
case "Less" -> actualPageCount < pageCount;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); boolean valid;
return null; try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
int actualPageCount = document.getNumberOfPages();
valid = compare(actualPageCount, pageCount, comparator);
}
return valid
? WebResponseUtils.multiPartFileToWebResponse(inputFile)
: ResponseEntity.noContent().build();
} }
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
@Operation( @Operation(
summary = "Checks if a PDF is of a certain size", summary = "Checks if a PDF is of a certain size",
description = "Input:PDF Output:Boolean Type:SISO") description = "Input:PDF Output:Boolean Type:SISO")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "PDF passed filter",
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
@ApiResponse(
responseCode = "204",
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String standardPageSize = request.getStandardPageSize(); String standardPageSize = request.getStandardPageSize();
String comparator = request.getComparator(); String comparator = request.getComparator();
// Load the PDF final boolean valid;
PDDocument document = pdfDocumentFactory.load(inputFile); try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
PDPage firstPage = document.getPage(0);
PDRectangle actualPageSize = firstPage.getMediaBox();
PDPage firstPage = document.getPage(0); float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
PDRectangle actualPageSize = firstPage.getMediaBox(); PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
float standardArea = standardSize.getWidth() * standardSize.getHeight();
// Calculate the area of the actual page size valid = compare(actualArea, standardArea, comparator);
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); }
// Get the standard size and calculate its area return valid
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); ? WebResponseUtils.multiPartFileToWebResponse(inputFile)
float standardArea = standardSize.getWidth() * standardSize.getHeight(); : ResponseEntity.noContent().build();
// Perform the comparison
boolean valid =
switch (comparator) {
case "Greater" -> actualArea > standardArea;
case "Equal" -> actualArea == standardArea;
case "Less" -> actualArea < standardArea;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
} }
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
@Operation( @Operation(
summary = "Checks if a PDF is a set file size", summary = "Checks if a PDF is a set file size",
description = "Input:PDF Output:Boolean Type:SISO") description = "Input:PDF Output:Boolean Type:SISO")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "PDF passed filter",
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
@ApiResponse(
responseCode = "204",
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
long fileSize = request.getFileSize(); long fileSize = request.getFileSize();
String comparator = request.getComparator(); String comparator = request.getComparator();
// Get the file size
long actualFileSize = inputFile.getSize(); long actualFileSize = inputFile.getSize();
boolean valid = compare(actualFileSize, fileSize, comparator);
// Perform the comparison return valid
boolean valid = ? WebResponseUtils.multiPartFileToWebResponse(inputFile)
switch (comparator) { : ResponseEntity.noContent().build();
case "Greater" -> actualFileSize > fileSize;
case "Equal" -> actualFileSize == fileSize;
case "Less" -> actualFileSize < fileSize;
default ->
throw ExceptionUtils.createInvalidArgumentException(
"comparator", comparator);
};
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
} }
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
@Operation( @Operation(
summary = "Checks if a PDF is of a certain rotation", summary = "Checks if a PDF is of a certain rotation",
description = "Input:PDF Output:Boolean Type:SISO") description = "Input:PDF Output:Boolean Type:SISO")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "PDF passed filter",
content = @Content(mediaType = MediaType.APPLICATION_PDF_VALUE)),
@ApiResponse(
responseCode = "204",
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
int rotation = request.getRotation(); int rotation = request.getRotation();
String comparator = request.getComparator(); String comparator = request.getComparator();
// Load the PDF boolean valid;
PDDocument document = pdfDocumentFactory.load(inputFile); try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
PDPage firstPage = document.getPage(0);
int actualRotation = firstPage.getRotation();
valid = compare(actualRotation, rotation, comparator);
}
// Get the rotation of the first page return valid
PDPage firstPage = document.getPage(0); ? WebResponseUtils.multiPartFileToWebResponse(inputFile)
int actualRotation = firstPage.getRotation(); : ResponseEntity.noContent().build();
}
// Perform the comparison /**
boolean valid = * Compares two values based on the provided comparator.
switch (comparator) { *
case "Greater" -> actualRotation > rotation; * @param <T> The type of the values being compared.
case "Equal" -> actualRotation == rotation; * @param actual The actual value.
case "Less" -> actualRotation < rotation; * @param expected The expected value.
default -> * @param comparator The comparator to use (e.g., "Greater", "Less", "Equal").
throw ExceptionUtils.createInvalidArgumentException( * @return True if the comparison is valid, false otherwise.
"comparator", comparator); */
}; private static <T extends Comparable<T>> boolean compare(
T actual, T expected, String comparator) {
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); return switch (comparator) {
return null; case "Greater" -> actual.compareTo(expected) > 0;
case "Equal" -> actual.compareTo(expected) == 0;
case "Less" -> actual.compareTo(expected) < 0;
default ->
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
};
} }
} }