Skip to content
This repository was archived by the owner on Jul 22, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ and what APIs have changed, if applicable.

## [Unreleased]

## [29.85.7-rc.5] - 2026-04-08

Testing darkcluster deop storm fix.

## [29.85.6] - 2026-03-06
- Add cluster subsetting configuration (enableClusterSubsetting, minClusterSubsetSize) to D2Cluster and ClusterProperties.

Expand Down Expand Up @@ -5973,7 +5977,8 @@ patch operations can re-use these classes for generating patch messages.

## [0.14.1]

[Unreleased]: https://github.com/linkedin/rest.li/compare/v29.85.6...master
[Unreleased]: https://github.com/linkedin/rest.li/compare/v29.85.7-rc.5...master
[29.85.7-rc.5]: https://github.com/linkedin/rest.li/compare/v29.85.6...v29.85.7-rc.5
[29.85.6]: https://github.com/linkedin/rest.li/compare/v29.85.5...v29.85.6
[29.85.5]: https://github.com/linkedin/rest.li/compare/v29.85.4...v29.85.5
[29.85.4]: https://github.com/linkedin/rest.li/compare/v29.85.3...v29.85.4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
import com.linkedin.jersey.api.uri.UriBuilder;
import com.linkedin.util.ArgumentUtil;
import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
Expand All @@ -29,7 +27,6 @@

public class D2URIRewriter implements URIRewriter
{
final private static Logger LOGGER = LoggerFactory.getLogger(D2URIRewriter.class);
final private URI _httpURI;

public D2URIRewriter(URI httpURI)
Expand All @@ -47,12 +44,8 @@ public URI rewriteURI(URI d2Uri)
{
builder.path(path);
}
builder.replaceQuery(d2Uri.getRawQuery());
builder.replaceQueryFrom(d2Uri);
builder.fragment(d2Uri.getRawFragment());
URI rewrittenUri = builder.build();

LOGGER.debug("rewrite uri {} -> {}", d2Uri, rewrittenUri);

return rewrittenUri;
return builder.build();
}
}
127 changes: 127 additions & 0 deletions d2/src/test/java/com/linkedin/d2/balancer/util/TestD2URIRewriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.client.utils.URIBuilder;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;


Expand All @@ -40,4 +43,128 @@ public void testSimpleD2Rewrite() throws URISyntaxException
URI finalURI = URIRewriter.rewriteURI(d2URI);
Assert.assertEquals(finalURI.toString(), expectURL);
}

@Test
public void testRewriteWithEncodedQuery() throws URISyntaxException
{
final URI httpURI = new URIBuilder("http://www.linkedin.com:1234/test").build();
final URI d2URI = URI.create("d2://serviceName/request/query?q=hello%20world&name=foo%26bar");
final String expectURL = "http://www.linkedin.com:1234/test/request/query?q=hello%20world&name=foo%26bar";

URI result = new D2URIRewriter(httpURI).rewriteURI(d2URI);
Assert.assertEquals(result.toString(), expectURL);
}

@Test
public void testRewriteNoQuery() throws URISyntaxException
{
final URI httpURI = new URIBuilder("http://www.linkedin.com:1234/test").build();
final URI d2URI = URI.create("d2://serviceName/request/path");
final String expectURL = "http://www.linkedin.com:1234/test/request/path";

URI result = new D2URIRewriter(httpURI).rewriteURI(d2URI);
Assert.assertEquals(result.toString(), expectURL);
}

@Test
public void testRewriteWithFragment() throws URISyntaxException
{
final URI httpURI = new URIBuilder("http://www.linkedin.com:1234").build();
final URI d2URI = URI.create("d2://serviceName/path?q=1#section");
final String expectURL = "http://www.linkedin.com:1234/path?q=1#section";

URI result = new D2URIRewriter(httpURI).rewriteURI(d2URI);
Assert.assertEquals(result.toString(), expectURL);
}

/**
* Verify that rewriting preserves percent-encoded characters in query strings.
* Uses {@link com.linkedin.jersey.api.uri.UriBuilder#replaceQueryFrom(URI)} which copies
* the raw query directly, avoiding character-by-character re-encoding.
*/
@DataProvider(name = "asciiQueryCharsEncoded")
public Object[][] asciiQueryCharsEncoded()
{
List<Object[]> cases = new ArrayList<>();
for (int c = 0x20; c <= 0x7E; c++)
{
// Every character in its percent-encoded form — this is how characters arrive
// in real RestRequest URIs from the HTTP layer.
String percentEncoded = String.format("%%%02X", c);
cases.add(new Object[]{
"percent-encoded 0x" + String.format("%02X", c) + " '" + (char) c + "'",
"/path?q=" + percentEncoded
});
}

// Realistic rest.li query strings
cases.add(new Object[]{"restli query", "/myResource/1?q=findByName&name=hello%20world"});
cases.add(new Object[]{"restli complex query",
"/myResource?q=search&keywords=java%20engineer&start=0&count=10&fields=id,firstName,lastName"});
cases.add(new Object[]{"restli batch get",
"/myResource?ids=List(1,2,3)&fields=id,name"});
cases.add(new Object[]{"query with encoded special chars",
"/myResource?filter=(key%3Avalue)&sort=name%26date"});
cases.add(new Object[]{"restli encoded brackets",
"/myResource?criteria%5B0%5D=valueA&criteria%5B1%5D=valueB"});

return cases.toArray(new Object[0][]);
}

@Test(dataProvider = "asciiQueryCharsEncoded")
public void testRewritePreservesEncodedQuery(String description, String uriString)
{
URI configuredURI = URI.create("d2://testService");
D2URIRewriter rewriter = new D2URIRewriter(configuredURI);

URI input = URI.create(uriString);
URI result = rewriter.rewriteURI(input);

// The raw query should be preserved exactly as-is
Assert.assertEquals(result.getRawQuery(), input.getRawQuery(),
"Query not preserved for [" + description + "] input=" + uriString);
}

@DataProvider(name = "asciiQueryCharsLiteral")
public Object[][] asciiQueryCharsLiteral()
{
List<Object[]> cases = new ArrayList<>();
for (int c = 0x20; c <= 0x7E; c++)
{
// # terminates query, % needs hex pair
if (c == '#' || c == '%')
{
continue;
}

String literal = "/path?q=" + (char) c;
try
{
URI.create(literal);
cases.add(new Object[]{
"literal 0x" + String.format("%02X", c) + " '" + (char) c + "'",
literal
});
}
catch (IllegalArgumentException e)
{
// Character not valid in URI.create — skip
}
}
return cases.toArray(new Object[0][]);
}

@Test(dataProvider = "asciiQueryCharsLiteral")
public void testRewritePreservesLiteralQuery(String description, String uriString)
{
URI configuredURI = URI.create("d2://testService");
D2URIRewriter rewriter = new D2URIRewriter(configuredURI);

URI input = URI.create(uriString);
URI result = rewriter.rewriteURI(input);

// The raw query should be preserved exactly as-is
Assert.assertEquals(result.getRawQuery(), input.getRawQuery(),
"Query not preserved for [" + description + "] input=" + uriString);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,17 @@ public void testRewriteWithQueryParams()
URI outputURI = rewriter.rewriteURI(inputUri);
Assert.assertEquals(outputURI, expectedURI, "URI's don't match");
}

@Test
public void testRewriteWithEncodedQueryParams()
{
String darkServiceName = "FooCluster-dark";
URI configuredURI = URI.create("d2://" + darkServiceName);
D2URIRewriter rewriter = new D2URIRewriter(configuredURI);

URI inputUri = URI.create("/MyRestliResource/foo/1?param1=hello%20world&param2=a%26b%3Dc");
URI expectedURI = URI.create("d2://"+ darkServiceName + "/MyRestliResource/foo/1?param1=hello%20world&param2=a%26b%3Dc");
URI outputURI = rewriter.rewriteURI(inputUri);
Assert.assertEquals(outputURI, expectedURI, "URI's don't match");
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version=29.85.6
version=29.85.7-rc.7
group=com.linkedin.pegasus
org.gradle.configureondemand=true
org.gradle.parallel=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,22 @@ public UriBuilder replaceQuery(String query) {
return this;
}

/**
* Replace the query string with the raw (already percent-encoded) query from the given URI.
* This avoids the expensive character-by-character re-encoding in {@link #replaceQuery(String)}
* when the source is a {@link URI} whose query is already properly encoded.
* @param uri the URI whose raw query string to copy
* @return this
*/
public UriBuilder replaceQueryFrom(URI uri) {
checkSsp();
this.query.setLength(0);
String rawQuery = uri.getRawQuery();
if (rawQuery != null)
this.query.append(rawQuery);
return this;
}

/**
* add the given queryParam and its value(s) to the UriBuilder
* @param name name of the queryParam
Expand Down
Loading