Skip to content

Commit 7dacfdc

Browse files
authored
docs: improve error messages (#1011)
* docs: improve error messages Improve error messages when something is wrong in the connection string. Fixes #java-spanner-jdbc/399 * fix: fix error message if emulator host is set * refactor: move local connection checker to separate class
1 parent 9132c21 commit 7dacfdc

File tree

4 files changed

+145
-2
lines changed

4 files changed

+145
-2
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ public String[] getValidValues() {
144144
}
145145
}
146146

147+
private static final LocalConnectionChecker LOCAL_CONNECTION_CHECKER =
148+
new LocalConnectionChecker();
147149
private static final boolean DEFAULT_USE_PLAIN_TEXT = false;
148150
static final boolean DEFAULT_AUTOCOMMIT = true;
149151
static final boolean DEFAULT_READONLY = false;
@@ -739,6 +741,7 @@ static List<String> parseProperties(String uri) {
739741
* @return a new {@link Connection} to the database referenced by this {@link ConnectionOptions}
740742
*/
741743
public Connection getConnection() {
744+
LOCAL_CONNECTION_CHECKER.checkLocalConnection(this);
742745
return new ConnectionImpl(this);
743746
}
744747

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/CredentialsService.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,18 @@ GoogleCredentials createCredentials(String credentialsUrl) {
5555
return getCredentialsFromUrl(credentialsUrl);
5656
}
5757
} catch (IOException e) {
58-
throw SpannerExceptionFactory.newSpannerException(
59-
ErrorCode.INVALID_ARGUMENT, "Invalid credentials path specified", e);
58+
String msg = "Invalid credentials path specified: ";
59+
if (credentialsUrl == null) {
60+
msg =
61+
msg
62+
+ "There are no credentials set in the connection string, "
63+
+ "and the default application credentials are not set or are pointing to an invalid or non-existing file.\n"
64+
+ "Please check the GOOGLE_APPLICATION_CREDENTIALS environment variable and/or "
65+
+ "the credentials that have been set using the Google Cloud SDK gcloud auth application-default login command";
66+
} else {
67+
msg = msg + credentialsUrl;
68+
}
69+
throw SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, msg, e);
6070
}
6171
}
6272

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.connection;
18+
19+
import com.google.api.gax.core.NoCredentialsProvider;
20+
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
21+
import com.google.api.gax.rpc.UnavailableException;
22+
import com.google.api.gax.rpc.UnimplementedException;
23+
import com.google.cloud.spanner.ErrorCode;
24+
import com.google.cloud.spanner.SpannerExceptionFactory;
25+
import com.google.cloud.spanner.admin.instance.v1.stub.GrpcInstanceAdminStub;
26+
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
27+
import com.google.spanner.admin.instance.v1.ListInstanceConfigsRequest;
28+
import java.io.IOException;
29+
import org.threeten.bp.Duration;
30+
31+
/**
32+
* Util class for quickly checking whether a local emulator or test server can be found. A common
33+
* configuration error is to add 'localhost' to the connection string or to forget to unset the
34+
* SPANNER_EMULATOR_HOST environment variable. This can cause cryptic error messages. This util
35+
* checks for common configurations and errors and returns a more understandable error message for
36+
* known misconfigurations.
37+
*/
38+
class LocalConnectionChecker {
39+
40+
/**
41+
* Executes a quick check to see if this connection can actually connect to a local emulator host
42+
* or other (mock) test server, if the options point to localhost instead of Cloud Spanner.
43+
*/
44+
void checkLocalConnection(ConnectionOptions options) {
45+
final String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
46+
String host = options.getHost() == null ? emulatorHost : options.getHost();
47+
if (host.startsWith("https://")) {
48+
host = host.substring(8);
49+
}
50+
if (host.startsWith("http://")) {
51+
host = host.substring(7);
52+
}
53+
// Only do the check if the host has been set to localhost.
54+
if (host != null && host.startsWith("localhost") && options.isUsePlainText()) {
55+
// Do a quick check to see if anything is actually running on the host.
56+
try {
57+
InstanceAdminStubSettings.Builder testEmulatorSettings =
58+
InstanceAdminStubSettings.newBuilder()
59+
.setCredentialsProvider(NoCredentialsProvider.create())
60+
.setTransportChannelProvider(
61+
InstantiatingGrpcChannelProvider.newBuilder().setEndpoint(host).build());
62+
testEmulatorSettings
63+
.listInstanceConfigsSettings()
64+
.setSimpleTimeoutNoRetries(Duration.ofSeconds(10L));
65+
try (GrpcInstanceAdminStub stub =
66+
GrpcInstanceAdminStub.create(testEmulatorSettings.build())) {
67+
stub.listInstanceConfigsCallable()
68+
.call(
69+
ListInstanceConfigsRequest.newBuilder()
70+
.setParent(String.format("projects/%s", options.getProjectId()))
71+
.build());
72+
}
73+
} catch (UnavailableException e) {
74+
String msg;
75+
if (options.getHost() != null) {
76+
msg =
77+
String.format(
78+
"The connection string '%s' contains host '%s', but no running"
79+
+ " emulator or other server could be found at that address.\n"
80+
+ "Please check the connection string and/or that the emulator is running.",
81+
options.getUri(), host);
82+
} else {
83+
msg =
84+
String.format(
85+
"The environment variable SPANNER_EMULATOR_HOST has been set to '%s', but no running"
86+
+ " emulator or other server could be found at that address.\n"
87+
+ "Please check the environment variable and/or that the emulator is running.",
88+
emulatorHost);
89+
}
90+
throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNAVAILABLE, msg);
91+
} catch (UnimplementedException e) {
92+
// Ignore, this is probably a local mock server.
93+
} catch (IOException e) {
94+
// Ignore, this method is not checking whether valid credentials have been set.
95+
}
96+
}
97+
}
98+
}

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import com.google.auth.oauth2.GoogleCredentials;
2525
import com.google.auth.oauth2.ServiceAccountCredentials;
2626
import com.google.cloud.NoCredentials;
27+
import com.google.cloud.spanner.ErrorCode;
28+
import com.google.cloud.spanner.SpannerException;
2729
import com.google.cloud.spanner.SpannerOptions;
2830
import java.util.Arrays;
2931
import org.junit.Test;
@@ -476,4 +478,34 @@ public void testMaxSessions() {
476478
assertThat(options.getMaxSessions()).isEqualTo(4000);
477479
assertThat(options.getSessionPoolOptions().getMaxSessions()).isEqualTo(4000);
478480
}
481+
482+
@Test
483+
public void testLocalConnectionError() {
484+
String uri =
485+
"cloudspanner://localhost:1/projects/test-project/instances/test-instance/databases/test-database?usePlainText=true";
486+
ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build();
487+
try (Connection connection = options.getConnection()) {
488+
fail("Missing expected exception");
489+
} catch (SpannerException e) {
490+
assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode());
491+
assertThat(e.getMessage())
492+
.contains(
493+
String.format(
494+
"The connection string '%s' contains host 'localhost:1', but no running", uri));
495+
}
496+
}
497+
498+
@Test
499+
public void testInvalidCredentials() {
500+
String uri =
501+
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=/some/non/existing/path";
502+
try {
503+
ConnectionOptions.newBuilder().setUri(uri).build();
504+
fail("Missing expected exception");
505+
} catch (SpannerException e) {
506+
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
507+
assertThat(e.getMessage())
508+
.contains("Invalid credentials path specified: /some/non/existing/path");
509+
}
510+
}
479511
}

0 commit comments

Comments
 (0)