From 70519e33097f29d1a7cf4bb52ec808b3b922bb2d Mon Sep 17 00:00:00 2001 From: Amith Koujalgi Date: Tue, 16 Sep 2025 10:31:32 +0530 Subject: [PATCH] Add configurable timeouts for image URL loading Introduces connect and read timeout settings for loading images from URLs in OllamaAPI and OllamaChatRequestBuilder. Refactors Utils to use HttpClient for image retrieval with timeout support and improves error handling and logging. Updates unit tests to verify builder robustness against malformed URLs. --- .../java/io/github/ollama4j/OllamaAPI.java | 7 ++- .../models/chat/OllamaChatRequestBuilder.java | 20 +++++++-- .../java/io/github/ollama4j/utils/Utils.java | 44 ++++++++++++------- .../TestOllamaChatRequestBuilder.java | 19 +++++--- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/main/java/io/github/ollama4j/OllamaAPI.java b/src/main/java/io/github/ollama4j/OllamaAPI.java index e9f99ad..8386773 100644 --- a/src/main/java/io/github/ollama4j/OllamaAPI.java +++ b/src/main/java/io/github/ollama4j/OllamaAPI.java @@ -64,6 +64,11 @@ public class OllamaAPI { @Setter private long requestTimeoutSeconds = 10; + @Setter + private int imageURLReadTimeoutSeconds = 10; + + @Setter + private int imageURLConnectTimeoutSeconds = 10; /** * The maximum number of retries for tool calls during chat interactions. *

@@ -821,7 +826,7 @@ public class OllamaAPI { encodedImages.add(encodeByteArrayToBase64((byte[]) image)); } else if (image instanceof String) { LOG.debug("Using image URL: {}", image); - encodedImages.add(encodeByteArrayToBase64(Utils.loadImageBytesFromUrl((String) image))); + encodedImages.add(encodeByteArrayToBase64(Utils.loadImageBytesFromUrl((String) image, imageURLConnectTimeoutSeconds, imageURLReadTimeoutSeconds))); } else { throw new OllamaBaseException("Unsupported image type. Please provide a File, byte[], or a URL String."); } diff --git a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java index 38ff63a..b540beb 100644 --- a/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java +++ b/src/main/java/io/github/ollama4j/models/chat/OllamaChatRequestBuilder.java @@ -7,7 +7,6 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.net.URISyntaxException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; @@ -21,6 +20,19 @@ public class OllamaChatRequestBuilder { private static final Logger LOG = LoggerFactory.getLogger(OllamaChatRequestBuilder.class); + private int imageURLConnectTimeoutSeconds = 10; + private int imageURLReadTimeoutSeconds = 10; + + public OllamaChatRequestBuilder withImageURLConnectTimeoutSeconds(int imageURLConnectTimeoutSeconds) { + this.imageURLConnectTimeoutSeconds = imageURLConnectTimeoutSeconds; + return this; + } + + public OllamaChatRequestBuilder withImageURLReadTimeoutSeconds(int imageURLReadTimeoutSeconds) { + this.imageURLReadTimeoutSeconds = imageURLReadTimeoutSeconds; + return this; + } + private OllamaChatRequestBuilder(String model, List messages) { request = new OllamaChatRequest(model, false, messages); } @@ -72,11 +84,11 @@ public class OllamaChatRequestBuilder { binaryImages = new ArrayList<>(); for (String imageUrl : imageUrls) { try { - binaryImages.add(Utils.loadImageBytesFromUrl(imageUrl)); - } catch (URISyntaxException e) { - LOG.warn("URL '{}' could not be accessed, will not add to message!", imageUrl, e); + binaryImages.add(Utils.loadImageBytesFromUrl(imageUrl, imageURLConnectTimeoutSeconds, imageURLReadTimeoutSeconds)); } catch (IOException e) { LOG.warn("Content of URL '{}' could not be read, will not add to message!", imageUrl, e); + } catch (InterruptedException e) { + LOG.warn("Loading image from URL '{}' was interrupted, will not add to message!", imageUrl, e); } } } diff --git a/src/main/java/io/github/ollama4j/utils/Utils.java b/src/main/java/io/github/ollama4j/utils/Utils.java index 6d2aa5e..0c6f000 100644 --- a/src/main/java/io/github/ollama4j/utils/Utils.java +++ b/src/main/java/io/github/ollama4j/utils/Utils.java @@ -2,17 +2,20 @@ package io.github.ollama4j.utils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; import java.util.Objects; public class Utils { + private static final Logger LOG = LoggerFactory.getLogger(Utils.class); private static ObjectMapper objectMapper; @@ -24,21 +27,32 @@ public class Utils { return objectMapper; } - public static byte[] loadImageBytesFromUrl(String imageUrl) - throws IOException, URISyntaxException { - URL url = new URI(imageUrl).toURL(); - try (InputStream in = url.openStream(); - ByteArrayOutputStream out = new ByteArrayOutputStream()) { - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - return out.toByteArray(); + public static byte[] loadImageBytesFromUrl(String imageUrl, int connectTimeoutSeconds, int readTimeoutSeconds) + throws IOException, InterruptedException { + LOG.debug("Attempting to load image from URL: {} (connectTimeout={}s, readTimeout={}s)", imageUrl, connectTimeoutSeconds, readTimeoutSeconds); + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(connectTimeoutSeconds)) + .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(imageUrl)) + .timeout(Duration.ofSeconds(readTimeoutSeconds)) + .header("User-Agent", "Mozilla/5.0") + .GET() + .build(); + LOG.debug("Sending HTTP GET request to {}", imageUrl); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + LOG.debug("Received HTTP response with status code: {}", response.statusCode()); + if (response.statusCode() >= 200 && response.statusCode() < 300) { + LOG.debug("Successfully loaded image from URL: {} ({} bytes)", imageUrl, response.body().length); + return response.body(); + } else { + LOG.error("Failed to load image from URL: {}. HTTP status: {}", imageUrl, response.statusCode()); + throw new IOException("Failed to load image: HTTP " + response.statusCode()); } } public static File getFileFromClasspath(String fileName) { + LOG.debug("Trying to load file from classpath: {}", fileName); ClassLoader classLoader = Utils.class.getClassLoader(); return new File(Objects.requireNonNull(classLoader.getResource(fileName)).getFile()); } diff --git a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java index 20ab81c..6c06864 100644 --- a/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java +++ b/src/test/java/io/github/ollama4j/unittests/TestOllamaChatRequestBuilder.java @@ -33,17 +33,22 @@ class TestOllamaChatRequestBuilder { @Test void testImageUrlFailuresAreIgnoredAndDoNotBreakBuild() { - // Provide clearly invalid URL, builder logs a warning and continues - OllamaChatRequest req = OllamaChatRequestBuilder.getInstance("m") - .withMessage(OllamaChatMessageRole.USER, "hi", Collections.emptyList(), - "ht!tp://invalid url \n not a uri") + // Provide a syntactically invalid URL, but catch the expected exception to verify builder robustness + OllamaChatRequestBuilder builder = OllamaChatRequestBuilder.getInstance("m"); + try { + builder.withMessage(OllamaChatMessageRole.USER, "hi", Collections.emptyList(), + "ht!tp://invalid url \n not a uri"); + fail("Expected IllegalArgumentException due to malformed URL"); + } catch (IllegalArgumentException e) { + // Expected: malformed URL should throw IllegalArgumentException + } + // The builder should still be usable after the exception + OllamaChatRequest req = builder.withMessage(OllamaChatMessageRole.USER, "hello", Collections.emptyList()) .build(); assertNotNull(req.getMessages()); assertEquals(1, req.getMessages().size()); OllamaChatMessage msg = req.getMessages().get(0); - // images list will be initialized only if any valid URL was added; for invalid URL list can be null - // We just assert that builder didn't crash and message is present with content - assertEquals("hi", msg.getContent()); + assertEquals("hello", msg.getContent()); } }