From 5501ceaf393e0a64a267ba125cf3b80882ba01de Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Mon, 15 Jun 2026 22:26:04 +0000 Subject: [PATCH] Avoid Uri reallocation in UriEncoder.encode() on the no-op path --- .../org/asynchttpclient/util/UriEncoder.java | 8 ++ .../asynchttpclient/util/UriEncoderTest.java | 75 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 client/src/test/java/org/asynchttpclient/util/UriEncoderTest.java diff --git a/client/src/main/java/org/asynchttpclient/util/UriEncoder.java b/client/src/main/java/org/asynchttpclient/util/UriEncoder.java index 92706d2926..d205aed6ff 100644 --- a/client/src/main/java/org/asynchttpclient/util/UriEncoder.java +++ b/client/src/main/java/org/asynchttpclient/util/UriEncoder.java @@ -20,6 +20,7 @@ import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Objects; import static org.asynchttpclient.util.MiscUtils.isNonEmpty; import static org.asynchttpclient.util.Utf8UrlEncoder.encodeAndAppendQuery; @@ -143,6 +144,13 @@ private String withQuery(final String query, final @Nullable List queryPa public Uri encode(Uri uri, @Nullable List queryParams) { String newPath = encodePath(uri.getPath()); String newQuery = encodeQuery(uri.getQuery(), queryParams); + // Common case (already-encoded URL, no extra query params): encoding changes nothing, so reuse the + // input Uri instead of allocating an identical copy. encodePath returns the same String instance + // when nothing needs escaping; newQuery may be a freshly built but equal String, so compare by + // value. Every other Uri field is copied unchanged, so equal path+query means an identical Uri. + if (newPath.equals(uri.getPath()) && Objects.equals(newQuery, uri.getQuery())) { + return uri; + } return new Uri(uri.getScheme(), uri.getUserInfo(), uri.getHost(), diff --git a/client/src/test/java/org/asynchttpclient/util/UriEncoderTest.java b/client/src/test/java/org/asynchttpclient/util/UriEncoderTest.java new file mode 100644 index 0000000000..3895796653 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/util/UriEncoderTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * 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 org.asynchttpclient.util; + +import org.asynchttpclient.Param; +import org.asynchttpclient.uri.Uri; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Tests {@link UriEncoder#encode(Uri, List)}'s fast path: when an already-encoded URL is encoded with no + * extra query params, encoding changes nothing, so the SAME Uri instance is returned (no wasted copy). + * When anything actually changes (added params, or a path/query needing escaping), a new equal-or-encoded + * Uri is returned. + */ +public class UriEncoderTest { + + private static final UriEncoder FIXING = UriEncoder.uriEncoder(false); + private static final UriEncoder RAW = UriEncoder.uriEncoder(true); + + @Test + public void returnsSameInstanceWhenNothingChanges() { + Uri uri = Uri.create("http://www.example.com/path/to/resource?a=1&b=2"); + assertSame(uri, FIXING.encode(uri, null), "clean URL + no params should reuse the input Uri"); + assertSame(uri, FIXING.encode(uri, Collections.emptyList()), "empty param list should also reuse it"); + assertSame(uri, RAW.encode(uri, null), "RAW on a clean URL should reuse the input Uri"); + } + + @Test + public void returnsSameInstanceWhenNoQueryAndNoParams() { + Uri uri = Uri.create("http://www.example.com/path"); + assertSame(uri, FIXING.encode(uri, null)); + } + + @Test + public void rebuildsWhenQueryParamsAdded() { + Uri uri = Uri.create("http://www.example.com/path?a=1"); + List params = Collections.singletonList(new Param("b", "2")); + Uri encoded = FIXING.encode(uri, params); + assertNotSame(uri, encoded, "adding a query param must produce a new Uri"); + assertEquals("a=1&b=2", encoded.getQuery()); + // Untouched fields are preserved. + assertEquals(uri.getScheme(), encoded.getScheme()); + assertEquals(uri.getHost(), encoded.getHost()); + assertEquals(uri.getPath(), encoded.getPath()); + } + + @Test + public void rebuildsWhenPathNeedsEncoding() { + // A space in the path must be percent-encoded by FIXING, so the result differs from the input. + Uri uri = Uri.create("http://www.example.com/a b"); + Uri encoded = FIXING.encode(uri, null); + assertNotSame(uri, encoded, "a path needing escaping must produce a new Uri"); + assertEquals("/a%20b", encoded.getPath()); + } +}