yuqi1129 commented on code in PR #14517:
URL: https://github.com/apache/gravitino/pull/14517#discussion_r4450716373


##########
catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java:
##########
@@ -174,46 +320,150 @@ static RuntimeException toSchemaException(GlueException 
e, String context) {
    * @return a Gravitino or standard Java runtime exception
    */
   static RuntimeException toTableException(GlueException e, String context) {
-    if (isAuthenticationFailure(e)) {
-      return toAuthenticationException(e, context);
-    }
     if (e instanceof EntityNotFoundException) {
       return new NoSuchTableException(e, "%s does not exist", context);
     }
     if (e instanceof AlreadyExistsException) {
       return new TableAlreadyExistsException(e, "%s already exists", context);
     }
-    if (e instanceof InvalidInputException) {
-      return ExceptionMessages.illegalArgument(context, e);
+    return toGenericException(e, context, null);
+  }
+
+  /**
+   * Converts a failure raised by the Iceberg SDK into a Gravitino exception. 
Iceberg reports Glue
+   * and S3 errors either as its own exception types or wrapped around the AWS 
SDK exception, so the
+   * cause chain is walked from the outside in and the first recognized type 
decides the result:
+   * Iceberg not-found, already-exists, validation and forbidden errors map to 
the matching
+   * Gravitino types, a commit conflict maps to {@link 
OptimisticLockException}, and AWS SDK causes
+   * go through {@link #toGenericException}. A commit whose state is unknown 
is reported as such,
+   * because the change may have been applied. {@link 
GravitinoRuntimeException}, {@link
+   * IllegalArgumentException} and {@link UnsupportedOperationException} are 
returned as is.
+   *
+   * @param e the failure raised by an Iceberg catalog call
+   * @param context description of the operation context for error messages
+   * @return a Gravitino or standard Java runtime exception
+   */
+  static RuntimeException toIcebergException(RuntimeException e, String 
context) {
+    if (e instanceof GravitinoRuntimeException
+        || e instanceof IllegalArgumentException
+        || e instanceof UnsupportedOperationException) {
+      return e;
     }
-    if (e instanceof AccessDeniedException) {
-      return new ForbiddenException(e, "Glue error: %s: %s", context, 
awsErrorDetail(e));
+    for (Throwable t = e; t != null; t = t.getCause() == t ? null : 
t.getCause()) {
+      if (t instanceof CommitStateUnknownException) {
+        // A local validation failure underneath means nothing was sent, so 
the commit did not
+        // happen. Any other cause, such as a transport failure, does not mean 
the commit failed.
+        Throwable invalid = findCause(t, 
org.apache.iceberg.exceptions.ValidationException.class);
+        if (invalid != null) {
+          return new IllegalArgumentException(
+              String.format("Invalid request for %s: %s", context, 
describe(invalid)), e);
+        }
+        return new RuntimeException(
+            String.format(
+                "%s: the commit state is unknown and the change may have been 
applied; check the"
+                    + " table before retrying: %s",
+                context, describe(t.getCause() != null ? t.getCause() : t)),
+            e);
+      }
+      if (t instanceof CommitFailedException) {
+        Throwable awsCause = findCause(t, SdkException.class);
+        return awsCause == null || awsCause instanceof 
ConcurrentModificationException
+            ? new OptimisticLockException(
+                e, "%s: the table was changed concurrently, retry: %s", 
context, describe(t))
+            : toGenericException((SdkException) awsCause, context, null);
+      }
+      if (t instanceof AlreadyExistsException) {
+        return new TableAlreadyExistsException(
+            e, "%s: %s", context, awsErrorDetail((AlreadyExistsException) t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.AlreadyExistsException) {
+        return new TableAlreadyExistsException(e, "%s: %s", context, 
describe(t));
+      }
+      if (t instanceof NoSuchNamespaceException) {
+        return new NoSuchSchemaException(e, "%s: %s", context, describe(t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.NoSuchTableException) {
+        return new NoSuchTableException(e, "%s: %s", context, describe(t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.NotFoundException) {
+        // Iceberg wraps a Glue EntityNotFoundException here; it may be the 
table or its database.
+        return new NotFoundException(e, "%s: %s", context, describe(t));

Review Comment:
   [Important] Preserve the missing-schema type when Iceberg creation fails 
because the Glue database does not exist.
   
   With an explicit S3 `location`, creation reaches the Iceberg builder without 
the earlier database-location lookup. Iceberg 1.11 wraps Glue 
`EntityNotFoundException` in its `NotFoundException`; this branch returns a 
generic Gravitino `NotFoundException`, although `TableCatalog.createTable()` 
promises `NoSuchSchemaException` for a missing schema. The Java client 
preserves that generic type, so CLI `CreateTable` bypasses `UNKNOWN_SCHEMA`, 
and Trino `CatalogConnectorMetadata.createTable()` bypasses 
`GRAVITINO_SCHEMA_NOT_EXISTS` and falls through to a generic internal error.
   
   An operations-level probe with that Iceberg exception chain reproduces the 
mismatch. Please pass operation/object context into the converter and map the 
creation failure to `NoSuchSchemaException`; cover both this wrapper and the 
raw `EntityNotFoundException` branch below. The existing converter-only test 
asserting generic `NotFoundException` does not verify this caller contract. 
This was not tested against real AWS.



##########
catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueExceptionConverter.java:
##########
@@ -174,46 +320,150 @@ static RuntimeException toSchemaException(GlueException 
e, String context) {
    * @return a Gravitino or standard Java runtime exception
    */
   static RuntimeException toTableException(GlueException e, String context) {
-    if (isAuthenticationFailure(e)) {
-      return toAuthenticationException(e, context);
-    }
     if (e instanceof EntityNotFoundException) {
       return new NoSuchTableException(e, "%s does not exist", context);
     }
     if (e instanceof AlreadyExistsException) {
       return new TableAlreadyExistsException(e, "%s already exists", context);
     }
-    if (e instanceof InvalidInputException) {
-      return ExceptionMessages.illegalArgument(context, e);
+    return toGenericException(e, context, null);
+  }
+
+  /**
+   * Converts a failure raised by the Iceberg SDK into a Gravitino exception. 
Iceberg reports Glue
+   * and S3 errors either as its own exception types or wrapped around the AWS 
SDK exception, so the
+   * cause chain is walked from the outside in and the first recognized type 
decides the result:
+   * Iceberg not-found, already-exists, validation and forbidden errors map to 
the matching
+   * Gravitino types, a commit conflict maps to {@link 
OptimisticLockException}, and AWS SDK causes
+   * go through {@link #toGenericException}. A commit whose state is unknown 
is reported as such,
+   * because the change may have been applied. {@link 
GravitinoRuntimeException}, {@link
+   * IllegalArgumentException} and {@link UnsupportedOperationException} are 
returned as is.
+   *
+   * @param e the failure raised by an Iceberg catalog call
+   * @param context description of the operation context for error messages
+   * @return a Gravitino or standard Java runtime exception
+   */
+  static RuntimeException toIcebergException(RuntimeException e, String 
context) {
+    if (e instanceof GravitinoRuntimeException
+        || e instanceof IllegalArgumentException
+        || e instanceof UnsupportedOperationException) {
+      return e;
     }
-    if (e instanceof AccessDeniedException) {
-      return new ForbiddenException(e, "Glue error: %s: %s", context, 
awsErrorDetail(e));
+    for (Throwable t = e; t != null; t = t.getCause() == t ? null : 
t.getCause()) {
+      if (t instanceof CommitStateUnknownException) {
+        // A local validation failure underneath means nothing was sent, so 
the commit did not
+        // happen. Any other cause, such as a transport failure, does not mean 
the commit failed.
+        Throwable invalid = findCause(t, 
org.apache.iceberg.exceptions.ValidationException.class);
+        if (invalid != null) {
+          return new IllegalArgumentException(
+              String.format("Invalid request for %s: %s", context, 
describe(invalid)), e);
+        }
+        return new RuntimeException(
+            String.format(
+                "%s: the commit state is unknown and the change may have been 
applied; check the"
+                    + " table before retrying: %s",
+                context, describe(t.getCause() != null ? t.getCause() : t)),
+            e);
+      }
+      if (t instanceof CommitFailedException) {
+        Throwable awsCause = findCause(t, SdkException.class);
+        return awsCause == null || awsCause instanceof 
ConcurrentModificationException
+            ? new OptimisticLockException(
+                e, "%s: the table was changed concurrently, retry: %s", 
context, describe(t))
+            : toGenericException((SdkException) awsCause, context, null);
+      }
+      if (t instanceof AlreadyExistsException) {
+        return new TableAlreadyExistsException(
+            e, "%s: %s", context, awsErrorDetail((AlreadyExistsException) t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.AlreadyExistsException) {
+        return new TableAlreadyExistsException(e, "%s: %s", context, 
describe(t));
+      }
+      if (t instanceof NoSuchNamespaceException) {
+        return new NoSuchSchemaException(e, "%s: %s", context, describe(t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.NoSuchTableException) {
+        return new NoSuchTableException(e, "%s: %s", context, describe(t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.NotFoundException) {
+        // Iceberg wraps a Glue EntityNotFoundException here; it may be the 
table or its database.
+        return new NotFoundException(e, "%s: %s", context, describe(t));
+      }
+      if (t instanceof org.apache.iceberg.exceptions.ValidationException) {
+        return new IllegalArgumentException(
+            String.format("Invalid request for %s: %s", context, describe(t)), 
e);
+      }
+      if (t instanceof org.apache.iceberg.exceptions.ForbiddenException) {
+        // A Glue denial deeper in the chain keeps its AWS error code and 
message through the
+        // shared mapping; a denial by another service (such as S3) stays 
forbidden.
+        Throwable awsCause = findCause(t, SdkException.class);
+        return awsCause instanceof GlueException
+            ? toGenericException((SdkException) awsCause, context, null)
+            : new ForbiddenException(
+                e, "%s: %s", context, describe(awsCause != null ? awsCause : 
t));
+      }
+      if (t instanceof EntityNotFoundException) {
+        // Not wrapped by Iceberg; it may be the table or its database.
+        return new NotFoundException(e, "%s: %s", context, describe(t));
+      }
+      if (t instanceof SdkException) {
+        return toGenericException((SdkException) t, context, null);
+      }
     }
-    return new RuntimeException("Glue error: " + context + ": " + 
awsErrorDetail(e), e);
+    return new RuntimeException(context + ": " + describe(e), e);
   }
 
-  private static RuntimeException toAuthenticationException(GlueException e, 
String context) {
-    return new RuntimeException(
-        String.format(
-            "Failed to authenticate with AWS Glue for %s. AWS rejected the 
configured "
-                + "credentials. Verify the '%s' and '%s' catalog properties, 
or the configured "
-                + "default AWS credential source. AWS error: %s",
-            context,
-            GlueConstants.AWS_ACCESS_KEY_ID,
-            GlueConstants.AWS_SECRET_ACCESS_KEY,
-            awsErrorDetail(e)),
-        e);
+  private static String regionMismatchMessage(GlueException e, @Nullable 
GlueClient client) {
+    GlueServiceClientConfiguration conf =
+        client == null ? null : client.serviceClientConfiguration();
+    String region = conf != null && conf.region() != null ? conf.region().id() 
: "unknown";
+    String endpoint =
+        conf != null && conf.endpointOverride() != null && 
conf.endpointOverride().isPresent()
+            ? conf.endpointOverride().get().toString()
+            : "(not set)";
+    return String.format(
+        "The request was signed for a region that does not match the AWS Glue 
endpoint. Check that"
+            + " '%s' (%s) and '%s' (%s) refer to the same region. AWS error: 
%s",
+        GlueConstants.AWS_REGION,
+        region,
+        GlueConstants.AWS_GLUE_ENDPOINT,
+        endpoint,
+        awsErrorDetail(e));
+  }
+
+  @Nullable
+  private static Throwable findCause(Throwable t, Class<?> type) {
+    for (Throwable c = t.getCause(); c != null; c = c.getCause() == c ? null : 
c.getCause()) {
+      if (type.isInstance(c)) {
+        return c;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Describes a throwable for an error message without the name of the 
internal Iceberg catalog,
+   * which means nothing to the caller, falling back to the type name when 
there is no message.
+   */
+  private static String describe(Throwable t) {
+    String message = StringUtils.defaultIfBlank(t.getMessage(), 
t.getClass().getSimpleName());
+    return message.replace(GlueIcebergTableHelper.ICEBERG_CATALOG_NAME + ".", 
"");
   }
 
   /**
    * Renders the AWS-side detail of a Glue exception. AWS names the failing 
action and the resource
    * there, which is what the caller needs to act on; the error code is 
prefixed so the failure can
-   * be classified at a glance.
+   * be classified at a glance. Long messages, which quote the offending 
request value, are
+   * shortened in the middle so the violated constraint at the end stays 
visible.
    *
    * @param e the Glue exception to describe
    * @return the AWS error code and message, or a best-effort description when 
they are unavailable
    */
   private static String awsErrorDetail(GlueException e) {
+    return StringUtils.abbreviateMiddle(rawAwsErrorDetail(e), " ... ", 
MAX_AWS_MESSAGE_LENGTH);

Review Comment:
   [Important] This only bounds the outer message, not the default HTTP 
response or the Java client's final exception message.
   
   The converted exceptions retain the original AWS exception as their cause. 
`Utils.illegalArguments()` passes that exception into `ErrorResponse`, whose 
stack is generated with `printStackTrace()`. `includeErrorStackTrace` defaults 
to `true`, and the Java client's `ErrorHandlers.formatErrorMessage()` appends 
the received stack back to the message. In a serialization probe, a 
600,000-character AWS validation message passes the new outer-message length 
check, but the resulting `ErrorResponse` is still 612,500 characters. The 
original internal Iceberg catalog name can similarly reappear through the cause.
   
   Please cover the serialized response/client result, not just 
`ex.getMessage()`. Preserve the complete cause for server-side diagnosis while 
bounding or sanitizing the outward stack. If only the top-level message field 
is intended to be shortened, narrow the PR's user-facing claim and document the 
stack-setting limitation. The probe used the production `ErrorResponse` 
factory/DTO serialization, rather than an HTTP black-box test.



##########
catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalogPropertiesMetadata.java:
##########
@@ -19,86 +19,293 @@
 package org.apache.gravitino.catalog.glue;
 
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_ACCESS_KEY_ID;
+import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_API_CALL_ATTEMPT_TIMEOUT_MS;
+import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_API_CALL_TIMEOUT_MS;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_CATALOG_ID;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_ENDPOINT;
+import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_GLUE_MAX_ATTEMPTS;
 import static org.apache.gravitino.catalog.glue.GlueConstants.AWS_REGION;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.AWS_SECRET_ACCESS_KEY;
+import static 
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_AWS_GLUE_API_CALL_ATTEMPT_TIMEOUT_MS;
+import static 
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_AWS_GLUE_API_CALL_TIMEOUT_MS;
+import static 
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_AWS_GLUE_MAX_ATTEMPTS;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT_FILTER;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.DEFAULT_TABLE_FORMAT_VALUE;
 import static 
org.apache.gravitino.catalog.glue.GlueConstants.TABLE_FORMAT_FILTER;
 import static org.apache.gravitino.catalog.glue.GlueConstants.WAREHOUSE;
-import static 
org.apache.gravitino.connector.PropertyEntry.stringOptionalPropertyEntry;
-import static 
org.apache.gravitino.connector.PropertyEntry.stringRequiredPropertyEntry;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.cloud.storage.AWSPropertiesMetadata;
 import org.apache.gravitino.connector.BaseCatalogPropertiesMetadata;
 import org.apache.gravitino.connector.PropertyEntry;
+import software.amazon.awssdk.regions.Region;
 
 /** Properties metadata for the AWS Glue Data Catalog connector catalog-level 
configuration. */
 public class GlueCatalogPropertiesMetadata extends 
BaseCatalogPropertiesMetadata {
 
+  /** Accepted values of {@code default-table-format}, compared 
case-insensitively. */
+  static final Set<String> DEFAULT_TABLE_FORMATS = ImmutableSet.of("iceberg", 
"hive");
+
+  /** Accepted entries of {@code table-format-filter}, compared 
case-insensitively. */
+  static final Set<String> TABLE_FORMAT_FILTER_VALUES =
+      ImmutableSet.of(DEFAULT_TABLE_FORMAT_FILTER, "hive", "iceberg", "delta", 
"parquet");
+
+  private static final Pattern CATALOG_ID_PATTERN = Pattern.compile("\\d{12}");
+
+  private static final Pattern HOST_PORT_PATTERN =
+      Pattern.compile("[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?(:\\d{1,5})?");
+
   private static final Map<String, PropertyEntry<?>> PROPERTIES_METADATA =
       ImmutableMap.<String, PropertyEntry<?>>builder()
           .put(
               AWS_REGION,
-              stringRequiredPropertyEntry(
+              stringEntry(
                   AWS_REGION,
                   "AWS region for the Glue Data Catalog (e.g. us-east-1)",
+                  true /* required */,
                   true /* immutable */,
-                  false /* hidden */))
+                  null /* defaultValue */,
+                  GlueCatalogPropertiesMetadata::checkRegion))
           .put(
               AWS_GLUE_CATALOG_ID,
-              stringOptionalPropertyEntry(
+              stringEntry(
                   AWS_GLUE_CATALOG_ID,
                   "The 12-digit AWS account ID that owns the Glue catalog."
                       + " When omitted, defaults to the caller's AWS account 
ID.",
+                  false /* required */,
                   true /* immutable */,
                   null /* defaultValue */,
-                  false /* hidden */))
+                  GlueCatalogPropertiesMetadata::checkCatalogId))
           .put(AWS_ACCESS_KEY_ID, AWSPropertiesMetadata.AWS_ACCESS_KEY_ID)
           .put(AWS_SECRET_ACCESS_KEY, 
AWSPropertiesMetadata.AWS_SECRET_ACCESS_KEY)
           .put(
               AWS_GLUE_ENDPOINT,
-              stringOptionalPropertyEntry(
+              stringEntry(
                   AWS_GLUE_ENDPOINT,
                   "Custom Glue endpoint URL for VPC endpoints or LocalStack 
testing"
                       + " (e.g. http://localhost:4566)",
+                  false /* required */,
                   false /* immutable */,
                   null /* defaultValue */,
-                  false /* hidden */))
+                  GlueCatalogPropertiesMetadata::checkEndpoint))
+          .put(
+              AWS_GLUE_API_CALL_TIMEOUT_MS,
+              positiveLongEntry(
+                  AWS_GLUE_API_CALL_TIMEOUT_MS,
+                  "Upper bound in milliseconds for one Glue API call made by 
the catalog,"
+                      + " including retries. Not applied to Iceberg table 
I/O.",
+                  DEFAULT_AWS_GLUE_API_CALL_TIMEOUT_MS))
+          .put(
+              AWS_GLUE_API_CALL_ATTEMPT_TIMEOUT_MS,
+              positiveLongEntry(
+                  AWS_GLUE_API_CALL_ATTEMPT_TIMEOUT_MS,
+                  "Upper bound in milliseconds for a single attempt of a Glue 
API call.",
+                  DEFAULT_AWS_GLUE_API_CALL_ATTEMPT_TIMEOUT_MS))
+          .put(
+              AWS_GLUE_MAX_ATTEMPTS,
+              new PropertyEntry.Builder<Integer>()
+                  .withName(AWS_GLUE_MAX_ATTEMPTS)
+                  .withDescription(
+                      "Maximum number of attempts of a Glue API call made by 
the catalog,"
+                          + " including the first one. Not applied to Iceberg 
table I/O.")
+                  .withJavaType(Integer.class)
+                  .withDefaultValue(DEFAULT_AWS_GLUE_MAX_ATTEMPTS)
+                  .withDecoder(
+                      value -> {
+                        int attempts = Integer.parseInt(value.trim());
+                        Preconditions.checkArgument(attempts > 0, "must be 
positive");
+                        return attempts;
+                      })
+                  .withEncoder(String::valueOf)
+                  .build())
           .put(
               DEFAULT_TABLE_FORMAT,
-              stringOptionalPropertyEntry(
+              stringEntry(
                   DEFAULT_TABLE_FORMAT,
                   "Default format for tables created via createTable(). 
Accepted: iceberg, hive.",
+                  false /* required */,
                   false /* immutable */,
                   DEFAULT_TABLE_FORMAT_VALUE,
-                  false /* hidden */))
+                  GlueCatalogPropertiesMetadata::checkDefaultTableFormat))
           .put(
               TABLE_FORMAT_FILTER,
-              stringOptionalPropertyEntry(
+              stringEntry(
                   TABLE_FORMAT_FILTER,
-                  "Comma-separated table formats exposed by listTables() and 
loadTable().",
+                  "Comma-separated table formats exposed by the catalog. 
Tables of other formats"
+                      + " are absent from list, load, alter and drop, and 
cannot be created."
+                      + " Accepted: all, hive, iceberg, delta, parquet.",
+                  false /* required */,
                   false /* immutable */,
                   DEFAULT_TABLE_FORMAT_FILTER,
-                  false /* hidden */))
+                  GlueCatalogPropertiesMetadata::checkTableFormatFilter))
           .put(
               WAREHOUSE,
-              stringRequiredPropertyEntry(
+              stringEntry(
                   WAREHOUSE,
                   "Base storage path used as warehouse when no explicit 
location is set"
                       + " at table creation time and the database declares no 
LocationUri"
                       + " (e.g. s3://my-bucket/warehouse)."
                       + " Table location is then derived as 
warehouse/database/table.",
+                  true /* required */,
                   false /* immutable */,
-                  false /* hidden */))
+                  null /* defaultValue */,
+                  GlueCatalogPropertiesMetadata::checkWarehouse))
           .build();
 
   @Override
   protected Map<String, PropertyEntry<?>> specificPropertyEntries() {
     return PROPERTIES_METADATA;
   }
+
+  /**
+   * Parses {@code aws-glue-endpoint} into a URI with an {@code http} or 
{@code https} scheme and a
+   * host.
+   *
+   * @param endpoint the configured endpoint
+   * @return the parsed endpoint URI
+   * @throws IllegalArgumentException if the endpoint is not such a URI
+   */
+  static URI parseEndpoint(String endpoint) {
+    URI uri;
+    try {
+      uri = new URI(endpoint);
+    } catch (URISyntaxException e) {
+      throw new IllegalArgumentException(
+          String.format("Property '%s' is not a valid URI: %s", 
AWS_GLUE_ENDPOINT, e.getMessage()),
+          e);
+    }
+    Preconditions.checkArgument(
+        ("http".equalsIgnoreCase(uri.getScheme()) || 
"https".equalsIgnoreCase(uri.getScheme()))
+            && hasHost(uri),
+        "Property '%s' must be an http or https URL with a host, got: %s",
+        AWS_GLUE_ENDPOINT,
+        endpoint);
+    return uri;
+  }
+
+  private static boolean hasHost(URI uri) {
+    // getHost() is null for host names with '_', such as Docker service 
names, so such an
+    // authority is accepted when it is a plain host name with an optional 
port.
+    return uri.getHost() != null

Review Comment:
   [Important] A hostname is not enough to establish a usable endpoint: the 
port range is still unchecked.
   
   For example, `http://localhost:70000` passes `parseEndpoint()` because 
`URI.getHost()` is non-null. The underscore-host fallback also accepts 
five-digit ports above 65535. These values pass the property decoder and 
create-time local validation, allowing an unusable catalog to be stored until 
an operation attempts to connect.
   
   Please validate that an explicit port is in `1..65535`, applying the same 
numeric check to the underscore-host fallback while retaining valid no-port and 
IPv6 endpoints. A decoder probe expecting rejection of port 70000 fails on this 
head; a separate probe confirms the existing `moto_server:5000` endpoint can 
still build a real Glue client. No network request to the invalid port was 
needed.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to