Skip to content

Commit ea05afd

Browse files
committed
Fixed issue with 503 being not handled. Reorganized code handing status code to be more certain about codes
1 parent e1018a7 commit ea05afd

6 files changed

Lines changed: 146 additions & 50 deletions

File tree

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1716,7 +1716,7 @@ public CompletableFuture<QueryResponse> query(String sqlQuery, Map<String, Objec
17161716
return new QueryResponse(transportResp, responseFormat, requestSettings, metrics);
17171717

17181718
} catch (Exception e) {
1719-
ClientUtils.quiteClose(transportResp, LOG);
1719+
ClientUtils.quietClose(transportResp, LOG);
17201720
String msg = requestExMsg("Query", (i + 1), durationSince(startTime).toMillis(), requestSettings.getQueryId());
17211721
lastException = httpClientHelper.wrapException(msg, e, requestSettings.getQueryId());
17221722
if (httpClientHelper.shouldRetry(e, requestSettings.getAllSettings())) {

client-v2/src/main/java/com/clickhouse/client/api/internal/ClientUtils.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import org.slf4j.Logger;
44

55
import java.io.Closeable;
6-
import java.io.IOException;
76

87
/**
98
* Class containing utility methods used across the client.
@@ -20,7 +19,7 @@ public static boolean isBlank(String str) {
2019
return str == null || str.trim().isEmpty();
2120
}
2221

23-
public static void quiteClose(Closeable closeable, Logger log) {
22+
public static void quietClose(Closeable closeable, Logger log) {
2423
if (closeable != null) {
2524
try {
2625
closeable.close();

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import com.clickhouse.client.api.transport.Endpoint;
1818
import com.clickhouse.client.api.transport.internal.TransportRequest;
1919
import com.clickhouse.client.api.transport.internal.TransportResponse;
20-
import com.clickhouse.client.config.ClickHouseDefaultSslContextProvider;
2120
import com.clickhouse.data.ClickHouseFormat;
2221
import net.jpountz.lz4.LZ4Factory;
2322
import org.apache.commons.compress.compressors.CompressorStreamFactory;
@@ -46,6 +45,7 @@
4645
import org.apache.hc.core5.http.HttpHeaders;
4746
import org.apache.hc.core5.http.HttpHost;
4847
import org.apache.hc.core5.http.HttpRequest;
48+
import org.apache.hc.core5.http.HttpResponse;
4949
import org.apache.hc.core5.http.HttpStatus;
5050
import org.apache.hc.core5.http.NoHttpResponseException;
5151
import org.apache.hc.core5.http.ProtocolException;
@@ -369,10 +369,7 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
369369
* @return exception object with server code
370370
*/
371371
public Exception readError(HttpPost req, ClassicHttpResponse httpResponse) {
372-
final Header serverQueryIdHeader = httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID);
373-
final Header clientQueryIdHeader = req.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID);
374-
final Header queryHeader = Stream.of(serverQueryIdHeader, clientQueryIdHeader).filter(Objects::nonNull).findFirst().orElse(null);
375-
final String queryId = queryHeader == null ? "" : queryHeader.getValue();
372+
final String queryId = getQueryId(httpResponse, req);
376373
int serverCode = getHeaderInt(httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_EXCEPTION_CODE), 0);
377374
try {
378375
return serverCode > 0 ? readClickHouseError(httpResponse.getEntity(), serverCode, queryId, httpResponse.getCode()) :
@@ -608,11 +605,6 @@ private static final class TransportResponseImpl implements TransportResponse {
608605
this.delegate = delegate;
609606
}
610607

611-
@Override
612-
public int getStatusCode() {
613-
return 0;
614-
}
615-
616608
@Override
617609
public ClickHouseFormat getDataFormat() {
618610
Header formatHeader = delegate.getFirstHeader(ClickHouseHttpProto.HEADER_FORMAT);
@@ -632,6 +624,7 @@ public String getQueryId() {
632624
}
633625

634626
@Override
627+
@SuppressWarnings("unchecked")
635628
public <T> T getDelegate() {
636629
return (T) delegate;
637630
}
@@ -680,6 +673,7 @@ private ClassicHttpResponse doPostRequest(Map<String, Object> requestConfig, Htt
680673
doPoolVent();
681674

682675
ClassicHttpResponse httpResponse = null;
676+
boolean closeResponse = true;
683677
HttpContext context = createRequestHttpContext(requestConfig);
684678
try {
685679
httpResponse = httpClient.executeOpen(null, req, context);
@@ -688,35 +682,58 @@ private ClassicHttpResponse doPostRequest(Map<String, Object> requestConfig, Htt
688682
httpResponse.getCode(),
689683
requestConfig));
690684

691-
if (httpResponse.getCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
692-
throw new ClientMisconfigurationException("Proxy authentication required. Please check your proxy settings.");
693-
} else if (httpResponse.getCode() == HttpStatus.SC_BAD_GATEWAY) {
694-
httpResponse.close();
695-
throw new ClientException("Server returned '502 Bad gateway'. Check network and proxy settings.");
696-
} else if (httpResponse.getCode() >= HttpStatus.SC_BAD_REQUEST || httpResponse.containsHeader(ClickHouseHttpProto.HEADER_EXCEPTION_CODE)) {
697-
try {
698-
throw readError(req, httpResponse);
699-
} finally {
700-
httpResponse.close();
701-
}
685+
if (httpResponse.containsHeader(ClickHouseHttpProto.HEADER_EXCEPTION_CODE)) {
686+
throw readError(req, httpResponse);
702687
}
703-
return httpResponse;
704688

689+
int statusCode = httpResponse.getCode();
690+
switch (statusCode) {
691+
case HttpStatus.SC_OK:
692+
closeResponse = false;
693+
return httpResponse;
694+
case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
695+
throw new ClientMisconfigurationException("Proxy authentication required. Please check your proxy settings.");
696+
case HttpStatus.SC_BAD_GATEWAY:
697+
throw new ClientException("Server returned '502 Bad gateway'. Check network and proxy settings.");
698+
case HttpStatus.SC_SERVICE_UNAVAILABLE:
699+
throw new ServerException(0, "Server returned '503 Service Unavailable'. Check network settings.",
700+
HttpStatus.SC_SERVICE_UNAVAILABLE, getQueryId(httpResponse, req));
701+
case HttpStatus.SC_BAD_REQUEST:
702+
case HttpStatus.SC_UNAUTHORIZED:
703+
case HttpStatus.SC_FORBIDDEN:
704+
case HttpStatus.SC_SERVER_ERROR:
705+
case HttpStatus.SC_NOT_FOUND:
706+
// ClickHouse usually uses SC_BAD_REQUEST and SC_SERVER_ERROR to return error.
707+
// SC_UNAUTHORIZED, SC_FORBIDDEN is for authentication
708+
// SC_NOT_FOUND can be returned by ClickHouse when path doesn't match database, but also by proxy
709+
// others we cannot handle properly
710+
throw readError(req, httpResponse);
711+
default:
712+
throw new ClientException("Unexpected result status " + statusCode);
713+
}
705714
} catch (UnknownHostException e) {
706-
ClientUtils.quiteClose(httpResponse, LOG);
707715
LOG.warn("Host '{}' unknown", req.getAuthority());
708716
throw e;
709717
} catch (ConnectException | NoRouteToHostException e) {
710-
ClientUtils.quiteClose(httpResponse, LOG);
711718
LOG.warn("Failed to connect to '{}': {}", req.getAuthority(), e.getMessage());
712719
throw e;
713720
} catch (Exception e) {
714-
ClientUtils.quiteClose(httpResponse, LOG);
715721
LOG.debug("Failed to execute request to '{}': {}", req.getAuthority(), e.getMessage(), e);
716722
throw e;
723+
} finally {
724+
if (closeResponse) {
725+
ClientUtils.quietClose(httpResponse, LOG);
726+
}
717727
}
718728
}
719729

730+
private String getQueryId(HttpResponse httpResponse, HttpPost httpRequest) {
731+
final Header serverQueryIdHeader = httpResponse.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID);
732+
final Header clientQueryIdHeader = httpRequest.getFirstHeader(ClickHouseHttpProto.HEADER_QUERY_ID);
733+
final Header queryHeader = Stream.of(serverQueryIdHeader, clientQueryIdHeader).filter(Objects::nonNull).findFirst().orElse(null);
734+
return queryHeader == null ? "" : queryHeader.getValue();
735+
}
736+
720737
private static final ContentType CONTENT_TYPE = ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), "UTF-8");
721738

722739
private void addHeaders(HttpPost req, Map<String, Object> requestConfig) {

client-v2/src/main/java/com/clickhouse/client/api/transport/internal/TransportResponse.java

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,6 @@
88

99
public interface TransportResponse extends Closeable {
1010

11-
/**
12-
* Transport status code translated to one of values:
13-
* <ul>
14-
* <li>503 - for service unavailable</li>
15-
* <li>500 - server error</li>
16-
* <li>400 - user error</li>
17-
* <li>404 - endpoint not found</li>
18-
* <li>403 - access not granted</li>
19-
* <li>401 - no authentication information</li>
20-
* <li>200 - ok</li>
21-
* </ul>
22-
* @return integer value of status code
23-
*/
24-
int getStatusCode();
25-
26-
2711
/**
2812
* Data format returned by server or calculated other way
2913
* @return data format

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,6 +2114,102 @@ public static Object[][] testHttpStatusCompressedBodyDataProvider() {
21142114
};
21152115
}
21162116

2117+
@Test(groups = {"integration"})
2118+
public void test503ServiceUnavailableSurfacesAsServerException() throws Exception {
2119+
if (isCloud()) {
2120+
return; // mocked server
2121+
}
2122+
2123+
WireMockServer mockServer = new WireMockServer(WireMockConfiguration
2124+
.options().dynamicPort().notifier(new ConsoleNotifier(false)));
2125+
mockServer.start();
2126+
2127+
try {
2128+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
2129+
.willReturn(WireMock.aResponse()
2130+
.withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE)
2131+
.withBody("Service Unavailable"))
2132+
.build());
2133+
2134+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false)
2135+
.setUsername("default")
2136+
.setPassword(ClickHouseServerForTest.getPassword())
2137+
.compressServerResponse(false)
2138+
.retryOnFailures(ClientFaultCause.None)
2139+
.build()) {
2140+
2141+
Throwable thrown = Assert.expectThrows(Throwable.class,
2142+
() -> client.query("SELECT 1").get(10, TimeUnit.SECONDS));
2143+
2144+
ServerException serverException = findServerException(thrown);
2145+
Assert.assertNotNull(serverException,
2146+
"Expected 503 to be reported as a ServerException, but was: " + thrown);
2147+
Assert.assertEquals(serverException.getTransportProtocolCode(), HttpStatus.SC_SERVICE_UNAVAILABLE,
2148+
"Expected transport protocol code 503, but was: " + serverException.getTransportProtocolCode());
2149+
2150+
Assert.assertTrue(containsMessageInCauseChain(thrown, "503 Service Unavailable"),
2151+
"Expected '503 Service Unavailable' in failure message chain, but was: " + thrown);
2152+
2153+
Assert.assertNull(findCause(thrown, ConnectionInitiationException.class),
2154+
"503 should not be reported as a ConnectionInitiationException, but was: " + thrown);
2155+
}
2156+
} finally {
2157+
mockServer.stop();
2158+
}
2159+
}
2160+
2161+
@Test(groups = {"integration"})
2162+
public void test503ServiceUnavailableIsRetried() throws Exception {
2163+
if (isCloud()) {
2164+
return; // mocked server
2165+
}
2166+
2167+
WireMockServer mockServer = new WireMockServer(WireMockConfiguration
2168+
.options().dynamicPort().notifier(new ConsoleNotifier(false)));
2169+
mockServer.start();
2170+
2171+
try {
2172+
// First request fails with a retryable 503 server error
2173+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
2174+
.inScenario("ServiceUnavailable")
2175+
.withRequestBody(WireMock.containing("SELECT 1"))
2176+
.whenScenarioStateIs(STARTED)
2177+
.willSetStateTo("Recovered")
2178+
.willReturn(WireMock.aResponse()
2179+
.withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE)
2180+
.withHeader("X-ClickHouse-Exception-Code", "202")
2181+
.withBody("Code: 202. DB::Exception: Too many simultaneous queries. (TOO_MANY_SIMULTANEOUS_QUERIES)"))
2182+
.build());
2183+
2184+
// Second request (retry) succeeds
2185+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
2186+
.inScenario("ServiceUnavailable")
2187+
.withRequestBody(WireMock.containing("SELECT 1"))
2188+
.whenScenarioStateIs("Recovered")
2189+
.willReturn(WireMock.aResponse()
2190+
.withStatus(HttpStatus.SC_OK)
2191+
.withHeader("X-ClickHouse-Summary", "{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}"))
2192+
.build());
2193+
2194+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false)
2195+
.setUsername("default")
2196+
.setPassword(ClickHouseServerForTest.getPassword())
2197+
.compressServerResponse(false)
2198+
.setMaxRetries(1)
2199+
.retryOnFailures(ClientFaultCause.ServerRetryable)
2200+
.build()) {
2201+
2202+
try (QueryResponse response = client.query("SELECT 1").get(10, TimeUnit.SECONDS)) {
2203+
Assert.assertNotNull(response);
2204+
}
2205+
}
2206+
2207+
mockServer.verify(2, WireMock.postRequestedFor(WireMock.anyUrl()));
2208+
} finally {
2209+
mockServer.stop();
2210+
}
2211+
}
2212+
21172213
private byte[] fetchBinaryPayload(String query, int networkBufferSize, int timeoutSec) throws Exception {
21182214
QuerySettings querySettings = new QuerySettings().setFormat(ClickHouseFormat.RowBinaryWithNamesAndTypes);
21192215
try (Client client = newClient()

client-v2/src/test/java/com/clickhouse/client/api/internal/ClientUtilsTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,36 @@
1111
public class ClientUtilsTest {
1212

1313
@Test(groups = {"unit"})
14-
public void testQuiteCloseSwallowsExceptionAndLogs() throws IOException {
14+
public void testQuietCloseSwallowsExceptionAndLogs() throws IOException {
1515
Logger log = Mockito.mock(Logger.class);
1616
IOException failure = new IOException("close failed");
1717
Closeable closeable = Mockito.mock(Closeable.class);
1818
Mockito.doThrow(failure).when(closeable).close();
1919

2020
// Should not propagate the exception thrown by close()
21-
ClientUtils.quiteClose(closeable, log);
21+
ClientUtils.quietClose(closeable, log);
2222

2323
Mockito.verify(closeable).close();
2424
Mockito.verify(log).warn(Mockito.contains("Failed to close object"), Mockito.eq(failure));
2525
}
2626

2727
@Test(groups = {"unit"})
28-
public void testQuiteCloseClosesSuccessfully() throws IOException {
28+
public void testQuietCloseClosesSuccessfully() throws IOException {
2929
Logger log = Mockito.mock(Logger.class);
3030
Closeable closeable = Mockito.mock(Closeable.class);
3131

32-
ClientUtils.quiteClose(closeable, log);
32+
ClientUtils.quietClose(closeable, log);
3333

3434
Mockito.verify(closeable).close();
3535
Mockito.verifyNoInteractions(log);
3636
}
3737

3838
@Test(groups = {"unit"})
39-
public void testQuiteCloseWithNull() {
39+
public void testQuietCloseWithNull() {
4040
Logger log = Mockito.mock(Logger.class);
4141

4242
// Should be a no-op and not throw on a null closeable
43-
ClientUtils.quiteClose(null, log);
43+
ClientUtils.quietClose(null, log);
4444

4545
Mockito.verifyNoInteractions(log);
4646
Assert.assertTrue(true);

0 commit comments

Comments
 (0)