44// you may not use this file except in compliance with the License.
55// You may obtain a copy of the License at
66//
7- // http://www.apache.org/licenses/LICENSE-2.0
7+ // http://www.apache.org/licenses/LICENSE-2.0
88//
99// Unless required by applicable law or agreed to in writing, software
1010// distributed under the License is distributed on an "AS IS" BASIS,
1111// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212// See the License for the specific language governing permissions and
1313// limitations under the License.
1414
15- package google .registry .mosapi . client ;
15+ package google .registry .mosapi ;
1616
1717import google .registry .config .RegistryConfig .Config ;
1818import google .registry .mosapi .exception .MosApiException ;
1919import google .registry .mosapi .exception .MosApiException .MosApiAuthorizationException ;
20- import google .registry .util .HttpUtils ;
2120import jakarta .inject .Inject ;
2221import jakarta .inject .Named ;
2322import jakarta .inject .Singleton ;
23+ import java .io .IOException ;
2424import java .net .HttpURLConnection ;
25- import java .net .URLEncoder ;
26- import java .net .http .HttpClient ;
27- import java .net .http .HttpResponse ;
28- import java .nio .charset .StandardCharsets ;
2925import java .util .Map ;
30- import java .util .stream .Collectors ;
26+ import okhttp3 .HttpUrl ;
27+ import okhttp3 .MediaType ;
28+ import okhttp3 .OkHttpClient ;
29+ import okhttp3 .Request ;
30+ import okhttp3 .RequestBody ;
31+ import okhttp3 .Response ;
3132
3233@ Singleton
3334public class MosApiClient {
3435
35- private final HttpClient httpClient ;
36+ private final OkHttpClient httpClient ;
3637 private final String baseUrl ;
3738
3839 @ Inject
3940 public MosApiClient (
40- @ Named ("mosapiHttpClient" ) HttpClient httpClient ,
41- @ Config ("mosapiUrl " ) String mosapiUrl ,
42- @ Config ("entityType " ) String entityType ) {
41+ @ Named ("mosapiHttpClient" ) OkHttpClient httpClient ,
42+ @ Config ("mosapiServiceUrl " ) String mosapiUrl ,
43+ @ Config ("mosapiEntityType " ) String entityType ) {
4344 this .httpClient = httpClient ;
44- this .baseUrl = String .format ("%s/%s" , mosapiUrl , entityType );
45+ // Pre-calculate base URL and validate it to fail fast on bad config
46+ String fullUrl = String .format ("%s/%s" , mosapiUrl , entityType );
47+ if (HttpUrl .parse (fullUrl ) == null ) {
48+ throw new IllegalArgumentException ("Invalid MoSAPI Service URL configuration: " + fullUrl );
49+ }
50+ this .baseUrl = fullUrl ;
4551 }
4652
4753 /**
@@ -51,45 +57,27 @@ public MosApiClient(
5157 * @param endpoint The specific API endpoint path (e.g., "v2/monitoring/state").
5258 * @param params A map of query parameters to be URL-encoded and appended to the request.
5359 * @param headers A map of HTTP headers to be included in the request.
54- * @return The {@link HttpResponse} from the server if the request is successful.
60+ * @return The {@link Response} from the server if the request is successful. <b>The caller is
61+ * responsible for closing this response.</b>
5562 * @throws MosApiException if the request fails due to a network error or an unhandled HTTP
5663 * status.
5764 * @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
5865 */
59- public HttpResponse <String > sendGetRequest (
60- String entityId , String endpoint , Map <String , String > params , Map <String , String > headers )
61- throws MosApiException {
62- String url = buildUrl (entityId , endpoint , params );
63- try {
64- HttpResponse <String > response = HttpUtils .sendGetRequest (httpClient , url , headers );
65- return checkResponseForAuthError (response );
66- } catch (RuntimeException e ) {
67- throw new MosApiException ("Error during GET request to " + url , e );
68- }
69- }
70-
71- /**
72- * Sends a GET request and decompresses the GZIP-encoded response body.
73- *
74- * @param entityId The TLD or registrar ID the request is for.
75- * @param endpoint The specific API endpoint path.
76- * @param params A map of query parameters to be URL-encoded.
77- * @param headers A map of HTTP headers to be included in the request.
78- * @return The decompressed {@link HttpResponse} from the server.
79- * @throws MosApiException if the request fails.
80- * @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
81- */
82- public HttpResponse <String > sendGetRequestWithDecompression (
66+ public Response sendGetRequest (
8367 String entityId , String endpoint , Map <String , String > params , Map <String , String > headers )
8468 throws MosApiException {
85- String url = buildUrl (entityId , endpoint , params );
69+ HttpUrl url = buildUri (entityId , endpoint , params );
70+ Request .Builder requestBuilder = new Request .Builder ().url (url ).get ();
71+ headers .forEach (requestBuilder ::addHeader );
8672 try {
87- HttpResponse <String > response =
88- HttpUtils .sendGetRequestWithDecompression (httpClient , url , headers );
73+ Response response = httpClient .newCall (requestBuilder .build ()).execute ();
8974 return checkResponseForAuthError (response );
90-
75+ } catch (MosApiAuthorizationException e ) {
76+ throw e ;
9177 } catch (RuntimeException e ) {
9278 throw new MosApiException ("Error during GET request to " + url , e );
79+ } catch (IOException e ) {
80+ throw new MosApiException ("IOException during GET request to " + url , e );
9381 }
9482 }
9583
@@ -104,29 +92,39 @@ public HttpResponse<String> sendGetRequestWithDecompression(
10492 * @param params A map of query parameters to be URL-encoded.
10593 * @param headers A map of HTTP headers to be included in the request.
10694 * @param body The request body to be sent with the POST request.
107- * @return The {@link HttpResponse} from the server.
95+ * @return The {@link Response} from the server. <b>The caller is responsible for closing this
96+ * response.</b>
10897 * @throws MosApiException if the request fails.
10998 * @throws MosApiAuthorizationException if the server returns a 401 Unauthorized status.
11099 */
111- public HttpResponse < String > sendPostRequest (
100+ public Response sendPostRequest (
112101 String entityId ,
113102 String endpoint ,
114103 Map <String , String > params ,
115104 Map <String , String > headers ,
116105 String body )
117106 throws MosApiException {
118- String url = buildUrl (entityId , endpoint , params );
107+ HttpUrl url = buildUri (entityId , endpoint , params );
108+ RequestBody requestBody = RequestBody .create (body , MediaType .parse ("application/json" ));
109+
110+ Request .Builder requestBuilder = new Request .Builder ().url (url ).post (requestBody );
111+ headers .forEach (requestBuilder ::addHeader );
119112 try {
120- HttpResponse < String > response = HttpUtils . sendPostRequest ( httpClient , url , headers , body );
113+ Response response = httpClient . newCall ( requestBuilder . build ()). execute ( );
121114 return checkResponseForAuthError (response );
115+ } catch (MosApiAuthorizationException e ) {
116+ throw e ;
122117 } catch (RuntimeException e ) {
123118 throw new MosApiException ("Error during POST request to " + url , e );
119+ } catch (IOException e ) {
120+ throw new MosApiException ("IOException during POST request to " + url , e );
124121 }
125122 }
126123
127- private HttpResponse < String > checkResponseForAuthError (HttpResponse < String > response )
124+ private Response checkResponseForAuthError (Response response )
128125 throws MosApiAuthorizationException {
129- if (response .statusCode () == HttpURLConnection .HTTP_UNAUTHORIZED ) {
126+ if (response .code () == HttpURLConnection .HTTP_UNAUTHORIZED ) {
127+ response .close ();
130128 throw new MosApiAuthorizationException (
131129 "Authorization failed for the requested resource. The client certificate may not be"
132130 + " authorized for the specified TLD or Registrar." );
@@ -137,21 +135,16 @@ private HttpResponse<String> checkResponseForAuthError(HttpResponse<String> resp
137135 /**
138136 * Builds the full URL for a request, including the base URL, entityId, path, and query params.
139137 */
140- private String buildUrl (String entityId , String path , Map <String , String > queryParams ) {
141- String sanitizedPath = path .startsWith ("/" ) ? path : "/" + path ;
142- String fullPath = "/" + entityId + sanitizedPath ;
138+ private HttpUrl buildUri (String entityId , String path , Map <String , String > queryParams ) {
139+ String sanitizedPath = path .startsWith ("/" ) ? path .substring (1 ) : path ;
140+
141+ // We can safely use get() here because we validated baseUrl in the constructor
142+ HttpUrl .Builder urlBuilder =
143+ HttpUrl .get (baseUrl ).newBuilder ().addPathSegment (entityId ).addPathSegments (sanitizedPath );
143144
144- if (queryParams == null || queryParams . isEmpty () ) {
145- return baseUrl + fullPath ;
145+ if (queryParams != null ) {
146+ queryParams . forEach ( urlBuilder :: addQueryParameter ) ;
146147 }
147- String queryString =
148- queryParams .entrySet ().stream ()
149- .map (
150- entry ->
151- entry .getKey ()
152- + "="
153- + URLEncoder .encode (entry .getValue (), StandardCharsets .UTF_8 ))
154- .collect (Collectors .joining ("&" ));
155- return baseUrl + fullPath + "?" + queryString ;
148+ return urlBuilder .build ();
156149 }
157150}
0 commit comments