Skip to content

Commit 873bf87

Browse files
SNOW-1898533: Do not set proxy in global request config (#2060)
1 parent 0607465 commit 873bf87

File tree

3 files changed

+163
-69
lines changed

3 files changed

+163
-69
lines changed

src/main/java/net/snowflake/client/core/HttpUtil.java

+34-50
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,13 @@ public static Duration getSocketTimeout() {
129129
@SnowflakeJdbcInternalApi
130130
public static void setConnectionTimeout(int timeout) {
131131
connectionTimeout = Duration.ofMillis(timeout);
132+
initDefaultRequestConfig(connectionTimeout.toMillis(), getSocketTimeout().toMillis());
132133
}
133134

134135
@SnowflakeJdbcInternalApi
135136
public static void setSocketTimeout(int timeout) {
136137
socketTimeout = Duration.ofMillis(timeout);
138+
initDefaultRequestConfig(getConnectionTimeout().toMillis(), socketTimeout.toMillis());
137139
}
138140

139141
public static long getDownloadedConditionTimeoutInSeconds() {
@@ -299,49 +301,11 @@ public static CloseableHttpClient buildHttpClient(
299301
socketTimeout,
300302
timeToLive);
301303

302-
// Set proxy settings for DefaultRequestConfig. If current proxy settings are the same as for
303-
// the last request, keep the current DefaultRequestConfig. If not, build a new
304-
// DefaultRequestConfig and set the new proxy settings for it
305-
HttpHost proxy =
306-
(key != null && key.usesProxy())
307-
? new HttpHost(
308-
key.getProxyHost(), key.getProxyPort(), key.getProxyHttpProtocol().getScheme())
309-
: null;
310-
// If defaultrequestconfig is not initialized or its proxy settings do not match current proxy
311-
// settings, re-build it (current or old proxy settings could be null, so null check is
312-
// included)
313-
boolean noDefaultRequestConfig =
314-
DefaultRequestConfig == null || DefaultRequestConfig.getProxy() == null;
315-
if (noDefaultRequestConfig || !DefaultRequestConfig.getProxy().equals(proxy)) {
316-
RequestConfig.Builder builder =
317-
RequestConfig.custom()
318-
.setConnectTimeout((int) connectTimeout)
319-
.setConnectionRequestTimeout((int) connectTimeout)
320-
.setSocketTimeout((int) socketTimeout);
321-
// only set the proxy settings if they are not null
322-
// but no value has been specified for nonProxyHosts
323-
// the route planner will determine whether to use a proxy based on nonProxyHosts value.
324-
String logMessage =
325-
"Rebuilding request config. Connect timeout: "
326-
+ connectTimeout
327-
+ " ms, connection request "
328-
+ "timeout: "
329-
+ connectTimeout
330-
+ " ms, socket timeout: "
331-
+ socketTimeout
332-
+ " ms";
333-
if (proxy != null && Strings.isNullOrEmpty(key.getNonProxyHosts())) {
334-
builder.setProxy(proxy);
335-
logMessage +=
336-
", host: "
337-
+ key.getProxyHost()
338-
+ ", port: "
339-
+ key.getProxyPort()
340-
+ ", scheme: "
341-
+ key.getProxyHttpProtocol().getScheme();
342-
}
343-
logger.debug(logMessage);
344-
DefaultRequestConfig = builder.build();
304+
// Create default request config without proxy since different connections could use different
305+
// proxies in multi tenant environments
306+
// Proxy is set later with route planner
307+
if (DefaultRequestConfig == null) {
308+
initDefaultRequestConfig(connectTimeout, socketTimeout);
345309
}
346310

347311
TrustManager[] trustManagers = null;
@@ -411,11 +375,18 @@ public static CloseableHttpClient buildHttpClient(
411375
.useSystemProperties()
412376
.setRedirectStrategy(new DefaultRedirectStrategy())
413377
.setUserAgent(buildUserAgent(userAgentSuffix)) // needed for Okta
414-
.disableCookieManagement(); // SNOW-39748
415-
378+
.disableCookieManagement() // SNOW-39748
379+
.setDefaultRequestConfig(DefaultRequestConfig);
416380
if (key != null && key.usesProxy()) {
381+
HttpHost proxy =
382+
new HttpHost(
383+
key.getProxyHost(), key.getProxyPort(), key.getProxyHttpProtocol().getScheme());
417384
logger.debug(
418-
"Instantiating proxy route planner with non-proxy hosts: {}", key.getNonProxyHosts());
385+
"Configuring proxy and route planner - host: {}, port: {}, scheme: {}, nonProxyHosts: {}",
386+
key.getProxyHost(),
387+
key.getProxyPort(),
388+
key.getProxyHttpProtocol().getScheme(),
389+
key.getNonProxyHosts());
419390
// use the custom proxy properties
420391
SnowflakeMutableProxyRoutePlanner sdkProxyRoutePlanner =
421392
httpClientRoutePlanner.computeIfAbsent(
@@ -426,7 +397,7 @@ public static CloseableHttpClient buildHttpClient(
426397
key.getProxyPort(),
427398
key.getProxyHttpProtocol(),
428399
key.getNonProxyHosts()));
429-
httpClientBuilder = httpClientBuilder.setProxy(proxy).setRoutePlanner(sdkProxyRoutePlanner);
400+
httpClientBuilder.setProxy(proxy).setRoutePlanner(sdkProxyRoutePlanner);
430401
if (!Strings.isNullOrEmpty(key.getProxyUser())
431402
&& !Strings.isNullOrEmpty(key.getProxyPassword())) {
432403
Credentials credentials =
@@ -440,20 +411,33 @@ public static CloseableHttpClient buildHttpClient(
440411
key.getProxyHost(),
441412
key.getProxyPort());
442413
credentialsProvider.setCredentials(authScope, credentials);
443-
httpClientBuilder = httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
414+
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
444415
}
445416
}
446-
httpClientBuilder.setDefaultRequestConfig(DefaultRequestConfig);
447417
if (downloadUnCompressed) {
448418
logger.debug("Disabling content compression for http client");
449-
httpClientBuilder = httpClientBuilder.disableContentCompression();
419+
httpClientBuilder.disableContentCompression();
450420
}
451421
return httpClientBuilder.build();
452422
} catch (NoSuchAlgorithmException | KeyManagementException ex) {
453423
throw new SSLInitializationException(ex.getMessage(), ex);
454424
}
455425
}
456426

427+
private static void initDefaultRequestConfig(long connectTimeout, long socketTimeout) {
428+
RequestConfig.Builder builder =
429+
RequestConfig.custom()
430+
.setConnectTimeout((int) connectTimeout)
431+
.setConnectionRequestTimeout((int) connectTimeout)
432+
.setSocketTimeout((int) socketTimeout);
433+
logger.debug(
434+
"Rebuilding request config. Connect timeout: {} ms, connection request timeout: {} ms, socket timeout: {} ms",
435+
connectTimeout,
436+
connectTimeout,
437+
socketTimeout);
438+
DefaultRequestConfig = builder.build();
439+
}
440+
457441
public static void updateRoutePlanner(HttpClientSettingsKey key) {
458442
if (httpClientRoutePlanner.containsKey(key)
459443
&& !httpClientRoutePlanner
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2025 Snowflake Computing Inc. All right reserved.
3+
*/
4+
package net.snowflake.client.core;
5+
6+
import static org.junit.jupiter.api.Assertions.assertNull;
7+
import static org.junit.jupiter.api.Assertions.fail;
8+
9+
import java.lang.reflect.Field;
10+
import java.util.AbstractMap;
11+
import java.util.Queue;
12+
import java.util.concurrent.ConcurrentLinkedQueue;
13+
import java.util.concurrent.CountDownLatch;
14+
import java.util.concurrent.TimeUnit;
15+
import org.apache.http.client.config.RequestConfig;
16+
import org.apache.http.client.methods.Configurable;
17+
import org.apache.http.impl.client.CloseableHttpClient;
18+
import org.hamcrest.CoreMatchers;
19+
import org.hamcrest.Matcher;
20+
import org.hamcrest.MatcherAssert;
21+
import org.junit.jupiter.api.Test;
22+
23+
public class HttpUtilTest {
24+
25+
/**
26+
* Test based on <a href="https://github.com/snowflakedb/snowflake-jdbc/issues/2047">reported
27+
* issue in SNOW-1898533</a>
28+
*/
29+
@Test
30+
public void buildHttpClientRace() throws InterruptedException {
31+
HttpUtil.httpClient.clear();
32+
// start two threads but only need one to fail
33+
CountDownLatch latch = new CountDownLatch(1);
34+
final Queue<AbstractMap.SimpleEntry<Thread, Throwable>> failures =
35+
new ConcurrentLinkedQueue<>();
36+
final HttpClientSettingsKey noProxyKey = new HttpClientSettingsKey(null);
37+
final HttpClientSettingsKey proxyKey =
38+
new HttpClientSettingsKey(
39+
null, "some.proxy.host", 8080, null, null, null, "http", null, false);
40+
41+
Thread noProxyThread =
42+
new Thread(() -> verifyProxyUsage(noProxyKey, failures, latch), "noProxyThread");
43+
noProxyThread.start();
44+
45+
Thread withProxyThread =
46+
new Thread(() -> verifyProxyUsage(proxyKey, failures, latch), "withProxyThread");
47+
withProxyThread.start();
48+
49+
// if latch goes to zero, then one of the threads failed
50+
// if await times out (returns false), then neither thread has failed (both still running)
51+
boolean failed = latch.await(1, TimeUnit.SECONDS);
52+
noProxyThread.interrupt();
53+
withProxyThread.interrupt();
54+
if (failed) {
55+
AbstractMap.SimpleEntry<Thread, Throwable> failure = failures.remove();
56+
fail(failure.getKey().getName() + " failed", failure.getValue());
57+
}
58+
}
59+
60+
private static void verifyProxyUsage(
61+
HttpClientSettingsKey key,
62+
Queue<AbstractMap.SimpleEntry<Thread, Throwable>> failures,
63+
CountDownLatch latch) {
64+
while (!Thread.currentThread().isInterrupted()) {
65+
try (CloseableHttpClient client = HttpUtil.buildHttpClient(key, null, false)) {
66+
assertHttpClientUsesProxy(client, key.usesProxy());
67+
} catch (Throwable e) {
68+
failures.add(new AbstractMap.SimpleEntry<>(Thread.currentThread(), e));
69+
latch.countDown();
70+
break;
71+
}
72+
}
73+
}
74+
75+
private static void assertHttpClientUsesProxy(CloseableHttpClient client, boolean proxyUsed) {
76+
assertRequestConfigWithoutProxyConfig(client);
77+
assertRoutePlannerOverridden(client, proxyUsed);
78+
}
79+
80+
private static void assertRequestConfigWithoutProxyConfig(CloseableHttpClient client) {
81+
MatcherAssert.assertThat(client, CoreMatchers.instanceOf(Configurable.class));
82+
Configurable c = (Configurable) client;
83+
RequestConfig config = c.getConfig();
84+
assertNull(config.getProxy(), "request config has configured proxy");
85+
}
86+
87+
private static void assertRoutePlannerOverridden(CloseableHttpClient client, boolean proxyUsed) {
88+
try {
89+
// HTTP client does not provide information about proxy settings so to detect that we are
90+
// using proxy we have to look inside via reflection and if the route planner is overridden to
91+
// our proxy class
92+
Field routePlannerField = client.getClass().getDeclaredField("routePlanner");
93+
routePlannerField.setAccessible(true);
94+
Matcher<Object> snowflakeProxyPlannerClassMatcher =
95+
CoreMatchers.instanceOf(SnowflakeMutableProxyRoutePlanner.class);
96+
MatcherAssert.assertThat(
97+
routePlannerField.get(client),
98+
proxyUsed
99+
? snowflakeProxyPlannerClassMatcher
100+
: CoreMatchers.not(snowflakeProxyPlannerClassMatcher));
101+
} catch (NoSuchFieldException | IllegalAccessException e) {
102+
throw new RuntimeException(e);
103+
}
104+
}
105+
}

src/test/java/net/snowflake/client/core/SessionUtilTest.java

+24-19
Original file line numberDiff line numberDiff line change
@@ -88,25 +88,30 @@ public void testParameterParsing() {
8888

8989
@Test
9090
public void testConvertSystemPropertyToIntValue() {
91-
// Test that setting real value works
92-
System.setProperty("net.snowflake.jdbc.max_connections", "500");
93-
assertEquals(
94-
500,
95-
SystemUtil.convertSystemPropertyToIntValue(
96-
HttpUtil.JDBC_MAX_CONNECTIONS_PROPERTY, HttpUtil.DEFAULT_MAX_CONNECTIONS));
97-
// Test that entering a non-int sets the value to the default
98-
System.setProperty("net.snowflake.jdbc.max_connections", "notAnInteger");
99-
assertEquals(
100-
HttpUtil.DEFAULT_MAX_CONNECTIONS,
101-
SystemUtil.convertSystemPropertyToIntValue(
102-
HttpUtil.JDBC_MAX_CONNECTIONS_PROPERTY, HttpUtil.DEFAULT_MAX_CONNECTIONS));
103-
// Test another system property
104-
System.setProperty("net.snowflake.jdbc.max_connections_per_route", "30");
105-
assertEquals(
106-
30,
107-
SystemUtil.convertSystemPropertyToIntValue(
108-
HttpUtil.JDBC_MAX_CONNECTIONS_PER_ROUTE_PROPERTY,
109-
HttpUtil.DEFAULT_MAX_CONNECTIONS_PER_ROUTE));
91+
try {
92+
// Test that setting real value works
93+
System.setProperty("net.snowflake.jdbc.max_connections", "500");
94+
assertEquals(
95+
500,
96+
SystemUtil.convertSystemPropertyToIntValue(
97+
HttpUtil.JDBC_MAX_CONNECTIONS_PROPERTY, HttpUtil.DEFAULT_MAX_CONNECTIONS));
98+
// Test that entering a non-int sets the value to the default
99+
System.setProperty("net.snowflake.jdbc.max_connections", "notAnInteger");
100+
assertEquals(
101+
HttpUtil.DEFAULT_MAX_CONNECTIONS,
102+
SystemUtil.convertSystemPropertyToIntValue(
103+
HttpUtil.JDBC_MAX_CONNECTIONS_PROPERTY, HttpUtil.DEFAULT_MAX_CONNECTIONS));
104+
// Test another system property
105+
System.setProperty("net.snowflake.jdbc.max_connections_per_route", "30");
106+
assertEquals(
107+
30,
108+
SystemUtil.convertSystemPropertyToIntValue(
109+
HttpUtil.JDBC_MAX_CONNECTIONS_PER_ROUTE_PROPERTY,
110+
HttpUtil.DEFAULT_MAX_CONNECTIONS_PER_ROUTE));
111+
} finally {
112+
System.clearProperty("net.snowflake.jdbc.max_connections");
113+
System.clearProperty("net.snowflake.jdbc.max_connections_per_route");
114+
}
110115
}
111116

112117
@Test

0 commit comments

Comments
 (0)