From 562b0bdd518bc030a5d37dfbf7e73ad76dad8d3e Mon Sep 17 00:00:00 2001 From: hangweizhang Date: Sun, 7 Jun 2026 10:57:20 +0800 Subject: [PATCH] fix(gemini): strip query string and fragment before extracting file extension GeminiMediaConverter.extractExtension() parsed the entire URL suffix after the last dot as the file extension, which included query strings and fragments. This caused URLs like http://localhost/cat.png?token=abc123 to produce extension "png?token=abc123" instead of "png", resulting in an IllegalArgumentException. Similarly, readFileAsBytes() failed for local file paths that contained query strings or fragments because Paths.get() treated them as part of the filename. Changes: - Add stripQueryAndFragment() utility to strip ?... and #... from URLs - Refactor extractExtension() to use stripQueryAndFragment() before extracting the extension - Refactor readFileAsBytes() to strip query/fragment from local file paths before resolving them on disk - Add test cases for URLs with query strings, fragments, and both Closes #1645 --- .../gemini/GeminiMediaConverter.java | 36 ++++++++++++++--- .../gemini/GeminiMediaConverterTest.java | 39 +++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java index cdaca84256..8b23d46cd9 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java +++ b/agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiMediaConverter.java @@ -128,7 +128,9 @@ private Part convertMediaBlockToInlineDataPart(Source source, String mediaType) /** * Read a file from URL/path as byte array. * - *

Supports both remote URLs (http://, https://) and local file paths. + *

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 @@ -146,8 +148,9 @@ 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); } @@ -155,6 +158,22 @@ private byte[] readFileAsBytes(String url) throws IOException { } } + /** + * Strips query string ({@code ?...}) and fragment ({@code #...}) from a URL/path string. + */ + private static String stripQueryAndFragment(String url) { + 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. * @@ -186,14 +205,19 @@ private String getMimeType(String url, String mediaType) { /** * Extract file extension from URL or path. * + *

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(); } } diff --git a/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMediaConverterTest.java b/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMediaConverterTest.java index 90c520dd6c..81daa27085 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMediaConverterTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiMediaConverterTest.java @@ -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();