Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
import com.google.common.base.Predicate;

public class ConfigOption<T> extends TypedOption<T, T> {
private boolean urlNormalize = false;
private String defaultScheme = null;

public ConfigOption<T> withUrlNormalization(String scheme) {
Comment thread
imbajin marked this conversation as resolved.
this.urlNormalize = true;
this.defaultScheme = scheme;
return this;
}

public boolean needsUrlNormalization() {
return this.urlNormalize;
}

public String getDefaultScheme() {
return this.defaultScheme;
}

public ConfigOption(String name, String desc, T value) {
this(name, desc, null, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,17 @@ private void setLayoutIfNeeded(Configuration conf) {
@SuppressWarnings("unchecked")
public <T, R> R get(TypedOption<T, R> option) {
Object value = this.getProperty(option.name());
boolean fromDefault = false;

if (value == null) {
return option.defaultValue();
value = option.defaultValue();
fromDefault = true;
}

if (!fromDefault) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code Quality: Inconsistent behavior between default and explicit values

Line 94-96 shows normalization only applies to explicitly set values (fromDefault = false), but not to default values from option.defaultValue().

This creates inconsistent behavior:

  • Default value http://127.0.0.1:8080 → used as-is
  • User sets 127.0.0.1:8080 → normalized to http://127.0.0.1:8080

Question: Is this intentional? If defaults already have schemes, why do we need .withUrlNormalization() on the options?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intentional.
The default values already have http://, so they are already correct. There is nothing to fix, so normalization is skipped. User values may not have the scheme (for example: 127.0.0.1:8080), so only those need normalization.
.withUrlNormalization() is just metadata. It tells the system: “this option is a URL, and use this scheme when fixing user input.”

So:
Default → already correct → no change
User value → may be incomplete → normalize

value = normalizeUrlOptionIfNeeded(option.name(), value);
}

return (R) value;
Comment on lines 91 to 104
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL normalization happens at retrieval time in the get() method rather than at storage time.

This means:

  1. the original unnormalized value remains in the underlying configuration storage and will be returned by direct access methods like getProperty() if they exist
  2. if the configuration is saved using save(), the unnormalized values will be written to the file, potentially causing confusion.
    Consider documenting this behavior or normalizing at storage time instead to maintain consistency.

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -213,4 +221,53 @@ private static Configuration loadConfigFile(File configFile) {
e, configFile);
}
}

private static Object normalizeUrlOptionIfNeeded(String key, Object value) {
if (value == null) {
return null;
}
Comment thread
bitflicker64 marked this conversation as resolved.

String scheme = defaultSchemeFor(key);
if (scheme == null) {
return value;
}

// Normalize URL config values by adding default scheme if missing.
Comment thread
bitflicker64 marked this conversation as resolved.
Outdated
// Only applies to allowlisted URL options (see defaultSchemeFor()).
if (value instanceof String) {
return prefixSchemeIfMissing((String) value, scheme);
}

// If it ever hits here, it means config storage returned a non-string type;
// leave it unchanged (safer than forcing toString()).
return value;
}

private static String defaultSchemeFor(String key) {
Comment thread
bitflicker64 marked this conversation as resolved.
Comment thread
bitflicker64 marked this conversation as resolved.
TypedOption<?, ?> option = OptionSpace.get(key);
if (option instanceof ConfigOption) {
ConfigOption<?> configOption = (ConfigOption<?>) option;
if (configOption.needsUrlNormalization()) {
return configOption.getDefaultScheme();
}
}
return null;
}

private static String prefixSchemeIfMissing(String raw, String scheme) {
if (raw == null) {
return null;
}
String s = raw.trim();
if (s.isEmpty()) {
return s;
}

// Keep original string if scheme already exists
Comment thread
bitflicker64 marked this conversation as resolved.
Outdated
String lower = s.toLowerCase();
if (lower.startsWith("http://") || lower.startsWith("https://")) {
return lower; // Return LOWERCASE version
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Important: Missing Edge Case Handling

The current implementation doesn't handle URLs with credentials or userinfo:

// These would incorrectly get prefixed:
"user:password@127.0.0.1:8080" → "http://user:password@127.0.0.1:8080"
"admin@localhost:8080""http://admin@localhost:8080"

While valid according to RFC 3986 (scheme://[userinfo@]host[:port]), detecting these requires checking for @ before any /:

Suggested change
}
// Keep original string if scheme already exists
String lower = s.toLowerCase();
if (lower.startsWith("http://") || lower.startsWith("https://")) {
return s;
}
// Don't add scheme if userinfo is present without scheme
// (e.g., "user:pass@host:port" - likely malformed or needs manual fixing)
int slashPos = s.indexOf('/');
int atPos = s.indexOf('@');
if (atPos != -1 && (slashPos == -1 || atPos < slashPos)) {
// Has userinfo component - preserve as-is to avoid masking config errors
return s;
}
return scheme + s;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefixSchemeIfMissing() method currently just normalizes URLs and doesn’t try to validate things like userinfo (user@host). That seems reasonable since handling credentials in URLs is discouraged and @ can also appear in valid paths. Adding special checks here would blur the line between normalization and validation.It’s probably better to handle stricter validation at the client/usage layer if needed.I’m happy to add a separate check or open an additional PR if you think stricter handling belongs here. Please let me know your preference.

return scheme + lower; // Return scheme + LOWERCASE input
Comment thread
bitflicker64 marked this conversation as resolved.
Outdated
Comment thread
imbajin marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class ServerOptions extends OptionHolder {
"The url for listening of graph server.",
disallowEmpty(),
"http://127.0.0.1:8080"
);
).withUrlNormalization("http://");

public static final ConfigOption<Integer> SERVER_EVENT_HUB_THREADS =
new ConfigOption<>(
Expand Down Expand Up @@ -118,7 +118,7 @@ public class ServerOptions extends OptionHolder {
"The url of gremlin server.",
disallowEmpty(),
"http://127.0.0.1:8182"
);
).withUrlNormalization("http://");

public static final ConfigOption<Integer> GREMLIN_SERVER_TIMEOUT =
new ConfigOption<>(
Expand Down Expand Up @@ -270,15 +270,15 @@ public class ServerOptions extends OptionHolder {
"to clients. only used when starting the server in k8s.",
disallowEmpty(),
"http://0.0.0.0:8080"
);
).withUrlNormalization("http://");
Comment thread
bitflicker64 marked this conversation as resolved.

public static final ConfigOption<String> SERVER_K8S_URL =
new ConfigOption<>(
"server.k8s_url",
"The url of k8s.",
disallowEmpty(),
"https://127.0.0.1:8888"
);
).withUrlNormalization("https://");

public static final ConfigOption<Boolean> SERVER_K8S_USE_CA =
new ConfigOption<>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You 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 org.apache.hugegraph.unit.config;

import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.config.OptionSpace;
import org.apache.hugegraph.config.ServerOptions;
import org.apache.hugegraph.testutil.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

public class ServerOptionsTest {

@BeforeClass
public static void init() {
OptionSpace.register("server",
ServerOptions.class.getName());
}

@Test
public void testUrlOptionNormalizeAddsDefaultScheme() {
PropertiesConfiguration conf = new PropertiesConfiguration();
conf.setProperty("restserver.url", "127.0.0.1:8080");
conf.setProperty("gremlinserver.url", "127.0.0.1:8182");
conf.setProperty("server.urls_to_pd", "0.0.0.0:8080");
conf.setProperty("server.k8s_url", "127.0.0.1:8888");

HugeConfig config = new HugeConfig(conf);

Assert.assertEquals("http://127.0.0.1:8080",
config.get(ServerOptions.REST_SERVER_URL));
Assert.assertEquals("http://127.0.0.1:8182",
config.get(ServerOptions.GREMLIN_SERVER_URL));
Assert.assertEquals("http://0.0.0.0:8080",
config.get(ServerOptions.SERVER_URLS_TO_PD));
Assert.assertEquals("https://127.0.0.1:8888",
config.get(ServerOptions.SERVER_K8S_URL));
}
Comment thread
bitflicker64 marked this conversation as resolved.

@Test
public void testUrlNormalizationEdgeCases() {
PropertiesConfiguration conf = new PropertiesConfiguration();
conf.setProperty("restserver.url", " 127.0.0.1:8080 ");
HugeConfig config = new HugeConfig(conf);
Assert.assertEquals("http://127.0.0.1:8080",
config.get(ServerOptions.REST_SERVER_URL));

conf = new PropertiesConfiguration();
conf.setProperty("restserver.url", "HTTP://127.0.0.1:8080");
config = new HugeConfig(conf);
Assert.assertEquals("http://127.0.0.1:8080",
config.get(ServerOptions.REST_SERVER_URL));
Comment thread
bitflicker64 marked this conversation as resolved.

conf = new PropertiesConfiguration();
conf.setProperty("restserver.url", "[::1]:8080");
config = new HugeConfig(conf);
Assert.assertEquals("http://[::1]:8080",
config.get(ServerOptions.REST_SERVER_URL));
Comment thread
bitflicker64 marked this conversation as resolved.
}
}
Loading