|
12 | 12 |
|
13 | 13 | import org.json.JSONObject; |
14 | 14 |
|
| 15 | +import airhacks.zsmith.configuration.control.HttpTimeouts; |
| 16 | + |
15 | 17 | public interface LinkCheckerTool { |
16 | 18 |
|
17 | | - int CONNECT_TIMEOUT_SECONDS = 10; |
18 | | - int REQUEST_TIMEOUT_SECONDS = 10; |
| 19 | + /// Deliberately shorter than the LLM transport timeouts: a reachability check should fail fast |
| 20 | + /// rather than block a tool turn. Overridable as ISO-8601 durations via `link.connect.timeout` |
| 21 | + /// and `link.request.timeout`. The errors — including timeouts — are returned as the tool |
| 22 | + /// result string, not thrown, so the model can react to them. |
| 23 | + Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); |
| 24 | + Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(10); |
19 | 25 | String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; |
20 | 26 |
|
21 | | - HttpClient HTTP_CLIENT = HttpClient.newBuilder() |
22 | | - .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)) |
23 | | - .followRedirects(Redirect.NORMAL) |
24 | | - .build(); |
| 27 | + static Duration connectTimeout() { |
| 28 | + return HttpTimeouts.duration("link.connect.timeout", DEFAULT_CONNECT_TIMEOUT); |
| 29 | + } |
| 30 | + |
| 31 | + static Duration requestTimeout() { |
| 32 | + return HttpTimeouts.duration("link.request.timeout", DEFAULT_REQUEST_TIMEOUT); |
| 33 | + } |
| 34 | + |
| 35 | + /// Built lazily so the configured connect timeout is read on first check, after config is |
| 36 | + /// loaded — never at interface initialization (which `create()` would trigger too early). |
| 37 | + static HttpClient client() { |
| 38 | + return Holder.instance(); |
| 39 | + } |
| 40 | + |
| 41 | + final class Holder { |
| 42 | + private static volatile HttpClient instance; |
| 43 | + |
| 44 | + static HttpClient instance() { |
| 45 | + var current = instance; |
| 46 | + if (current != null) { |
| 47 | + return current; |
| 48 | + } |
| 49 | + synchronized (Holder.class) { |
| 50 | + if (instance == null) { |
| 51 | + instance = HttpClient.newBuilder() |
| 52 | + .connectTimeout(connectTimeout()) |
| 53 | + .followRedirects(Redirect.NORMAL) |
| 54 | + .build(); |
| 55 | + } |
| 56 | + return instance; |
| 57 | + } |
| 58 | + } |
| 59 | + } |
25 | 60 |
|
26 | 61 | enum Field { url } |
27 | 62 |
|
@@ -72,12 +107,12 @@ private static String run(JSONObject input) { |
72 | 107 | private static HttpResponse<Void> send(URI uri, String method) throws IOException, InterruptedException { |
73 | 108 | var request = HttpRequest.newBuilder() |
74 | 109 | .uri(uri) |
75 | | - .timeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)) |
| 110 | + .timeout(requestTimeout()) |
76 | 111 | .header("User-Agent", USER_AGENT) |
77 | 112 | .header("Accept", "*/*") |
78 | 113 | .method(method, HttpRequest.BodyPublishers.noBody()) |
79 | 114 | .build(); |
80 | | - return HTTP_CLIENT.send(request, BodyHandlers.discarding()); |
| 115 | + return client().send(request, BodyHandlers.discarding()); |
81 | 116 | } |
82 | 117 |
|
83 | 118 | private static String format(HttpResponse<?> response) { |
|
0 commit comments