Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ private Part convertMediaBlockToInlineDataPart(Source source, String mediaType)
/**
* Read a file from URL/path as byte array.
*
* <p>Supports both remote URLs (http://, https://) and local file paths.
* <p>Supports both remote URLs (http://, https://) and local file paths. Query string
* and fragment are stripped from local file paths so that paths like {@code
* /tmp/cat.png?token=abc} resolve to the actual file on disk.
*
* @param url File URL or path
* @return File content as byte array
Expand All @@ -146,15 +148,32 @@ private byte[] readFileAsBytes(String url) throws IOException {
throw new IOException("Failed to download remote file: " + url, e);
}
} else {
// Local file path
Path path = Paths.get(url);
// Local file path — strip query string / fragment before resolving
String localPath = stripQueryAndFragment(url);
Path path = Paths.get(localPath);
if (!Files.exists(path)) {
throw new IOException("File not found: " + url);
}
return Files.readAllBytes(path);
}
}

/**
* Strips query string ({@code ?...}) and fragment ({@code #...}) from a URL/path string.
*/
private static String stripQueryAndFragment(String url) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[praise] Clean, focused utility method. The fix is applied consistently in both extractExtension() and readFileAsBytes(), the stripping order (fragment first, then query) correctly handles all URL forms including malformed ones like path#frag?query, and the error messages still reference the original URL for debugging. Good test coverage for the three main cases.

String path = url;
int fragmentIndex = path.indexOf('#');
if (fragmentIndex >= 0) {
path = path.substring(0, fragmentIndex);
}
int queryIndex = path.indexOf('?');
if (queryIndex >= 0) {
path = path.substring(0, queryIndex);
}
return path;
}

/**
* Determine MIME type from file extension.
*
Expand Down Expand Up @@ -186,14 +205,19 @@ private String getMimeType(String url, String mediaType) {
/**
* Extract file extension from URL or path.
*
* <p>Strips query string ({@code ?...}) and fragment ({@code #...}) before extracting
* the extension so that URLs like {@code http://example.com/cat.png?token=abc} produce
* {@code png} rather than {@code png?token=abc}.
*
* @param url File URL or path
* @return File extension in lowercase (without dot)
*/
private String extractExtension(String url) {
int lastDotIndex = url.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == url.length() - 1) {
String path = stripQueryAndFragment(url);
int lastDotIndex = path.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == path.length() - 1) {
throw new IllegalArgumentException("Cannot extract file extension from: " + url);
}
return url.substring(lastDotIndex + 1).toLowerCase();
return path.substring(lastDotIndex + 1).toLowerCase();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,45 @@ void testUnsupportedExtension() {
assertThrows(RuntimeException.class, () -> converter.convertToInlineDataPart(block));
}

@Test
void testUrlWithQueryString() {
// Issue #1645: URL with query string should strip ?... before extracting extension
URLSource source =
URLSource.builder().url(tempImageFile.toString() + "?token=abc123").build();
ImageBlock block = ImageBlock.builder().source(source).build();

// Should resolve to image/png and read the file successfully
Part result = converter.convertToInlineDataPart(block);
assertNotNull(result);
assertTrue(result.inlineData().isPresent());
assertEquals("image/png", result.inlineData().get().mimeType().get());
}

@Test
void testUrlWithFragment() {
// Issue #1645: URL with fragment should strip #... before extracting extension
URLSource source = URLSource.builder().url(tempAudioFile.toString() + "#preview").build();
AudioBlock block = AudioBlock.builder().source(source).build();

Part result = converter.convertToInlineDataPart(block);
assertNotNull(result);
assertTrue(result.inlineData().isPresent());
assertEquals("audio/mp3", result.inlineData().get().mimeType().get());
}

@Test
void testUrlWithQueryStringAndFragment() {
// Issue #1645: URL with both query string and fragment
URLSource source =
URLSource.builder().url(tempImageFile.toString() + "?download=1#preview").build();
ImageBlock block = ImageBlock.builder().source(source).build();

Part result = converter.convertToInlineDataPart(block);
assertNotNull(result);
assertTrue(result.inlineData().isPresent());
assertEquals("image/png", result.inlineData().get().mimeType().get());
}

@Test
void testFileNotFound() {
URLSource source = URLSource.builder().url("/nonexistent/file.png").build();
Expand Down
Loading