Skip to content
Permalink
Browse files
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
  • Loading branch information
olavloite committed Mar 28, 2021
1 parent 9132c21 commit 7dacfdc7ca1219a0ddf5929d7b46860b46e3c300
@@ -144,6 +144,8 @@ public String[] getValidValues() {
}
}

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

@@ -55,8 +55,18 @@ GoogleCredentials createCredentials(String credentialsUrl) {
return getCredentialsFromUrl(credentialsUrl);
}
} catch (IOException e) {
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Invalid credentials path specified", e);
String msg = "Invalid credentials path specified: ";
if (credentialsUrl == null) {
msg =
msg
+ "There are no credentials set in the connection string, "
+ "and the default application credentials are not set or are pointing to an invalid or non-existing file.\n"
+ "Please check the GOOGLE_APPLICATION_CREDENTIALS environment variable and/or "
+ "the credentials that have been set using the Google Cloud SDK gcloud auth application-default login command";
} else {
msg = msg + credentialsUrl;
}
throw SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, msg, e);
}
}

@@ -0,0 +1,98 @@
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.connection;

import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
import com.google.api.gax.rpc.UnavailableException;
import com.google.api.gax.rpc.UnimplementedException;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.admin.instance.v1.stub.GrpcInstanceAdminStub;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
import com.google.spanner.admin.instance.v1.ListInstanceConfigsRequest;
import java.io.IOException;
import org.threeten.bp.Duration;

/**
* Util class for quickly checking whether a local emulator or test server can be found. A common
* configuration error is to add 'localhost' to the connection string or to forget to unset the
* SPANNER_EMULATOR_HOST environment variable. This can cause cryptic error messages. This util
* checks for common configurations and errors and returns a more understandable error message for
* known misconfigurations.
*/
class LocalConnectionChecker {

/**
* Executes a quick check to see if this connection can actually connect to a local emulator host
* or other (mock) test server, if the options point to localhost instead of Cloud Spanner.
*/
void checkLocalConnection(ConnectionOptions options) {
final String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
String host = options.getHost() == null ? emulatorHost : options.getHost();
if (host.startsWith("https://")) {
host = host.substring(8);
}
if (host.startsWith("http://")) {
host = host.substring(7);
}
// Only do the check if the host has been set to localhost.
if (host != null && host.startsWith("localhost") && options.isUsePlainText()) {
// Do a quick check to see if anything is actually running on the host.
try {
InstanceAdminStubSettings.Builder testEmulatorSettings =
InstanceAdminStubSettings.newBuilder()
.setCredentialsProvider(NoCredentialsProvider.create())
.setTransportChannelProvider(
InstantiatingGrpcChannelProvider.newBuilder().setEndpoint(host).build());
testEmulatorSettings
.listInstanceConfigsSettings()
.setSimpleTimeoutNoRetries(Duration.ofSeconds(10L));
try (GrpcInstanceAdminStub stub =
GrpcInstanceAdminStub.create(testEmulatorSettings.build())) {
stub.listInstanceConfigsCallable()
.call(
ListInstanceConfigsRequest.newBuilder()
.setParent(String.format("projects/%s", options.getProjectId()))
.build());
}
} catch (UnavailableException e) {
String msg;
if (options.getHost() != null) {
msg =
String.format(
"The connection string '%s' contains host '%s', but no running"
+ " emulator or other server could be found at that address.\n"
+ "Please check the connection string and/or that the emulator is running.",
options.getUri(), host);
} else {
msg =
String.format(
"The environment variable SPANNER_EMULATOR_HOST has been set to '%s', but no running"
+ " emulator or other server could be found at that address.\n"
+ "Please check the environment variable and/or that the emulator is running.",
emulatorHost);
}
throw SpannerExceptionFactory.newSpannerException(ErrorCode.UNAVAILABLE, msg);
} catch (UnimplementedException e) {
// Ignore, this is probably a local mock server.
} catch (IOException e) {
// Ignore, this method is not checking whether valid credentials have been set.
}
}
}
}
@@ -24,6 +24,8 @@
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerOptions;
import java.util.Arrays;
import org.junit.Test;
@@ -476,4 +478,34 @@ public void testMaxSessions() {
assertThat(options.getMaxSessions()).isEqualTo(4000);
assertThat(options.getSessionPoolOptions().getMaxSessions()).isEqualTo(4000);
}

@Test
public void testLocalConnectionError() {
String uri =
"cloudspanner://localhost:1/projects/test-project/instances/test-instance/databases/test-database?usePlainText=true";
ConnectionOptions options = ConnectionOptions.newBuilder().setUri(uri).build();
try (Connection connection = options.getConnection()) {
fail("Missing expected exception");
} catch (SpannerException e) {
assertEquals(ErrorCode.UNAVAILABLE, e.getErrorCode());
assertThat(e.getMessage())
.contains(
String.format(
"The connection string '%s' contains host 'localhost:1', but no running", uri));
}
}

@Test
public void testInvalidCredentials() {
String uri =
"cloudspanner:/projects/test-project/instances/test-instance/databases/test-database?credentials=/some/non/existing/path";
try {
ConnectionOptions.newBuilder().setUri(uri).build();
fail("Missing expected exception");
} catch (SpannerException e) {
assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode());
assertThat(e.getMessage())
.contains("Invalid credentials path specified: /some/non/existing/path");
}
}
}

0 comments on commit 7dacfdc

Please sign in to comment.