From 283be4928f03f41fd3844d9d943c423a8a87e572 Mon Sep 17 00:00:00 2001 From: meysam Date: Wed, 4 Mar 2026 12:28:04 +0400 Subject: [PATCH 1/2] fix: restore error handling for non-2xx responses in GET and POST calls PR #54 changed builder.get(Class) to builder.get() and builder.post(Entity, Class) to builder.post(Entity) to access response headers. The typed overloads throw WebApplicationException on non-2xx responses, but the untyped overloads silently return the Response. Without an explicit status check, error responses were deserialized instead of raising AuthleteApiException. Add status family check after builder.get() and builder.post() to throw WebApplicationException on non-2xx, restoring the original error-handling contract. Add regression tests covering GET, POST, and DELETE paths. --- .../jaxrs/api/AuthleteApiJaxrsImpl.java | 18 +- .../jaxrs/api/AuthleteApiJaxrsImplTest.java | 258 ++++++++++++++++++ 2 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImplTest.java diff --git a/src/main/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImpl.java b/src/main/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImpl.java index 09a492e4..7f69e8ca 100644 --- a/src/main/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImpl.java +++ b/src/main/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImpl.java @@ -430,9 +430,16 @@ protected TResponse callGetApi( setCustomRequestHeaders(builder, options); Response httpResponse = builder.get(); + + if (httpResponse.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) + { + throw new WebApplicationException(httpResponse); + } + TResponse apiResponseObject = httpResponse.readEntity(responseClass); - if (apiResponseObject instanceof ApiResponse) { + if (apiResponseObject instanceof ApiResponse) + { ((ApiResponse) apiResponseObject).setResponseHeaders(httpResponse.getStringHeaders()); } @@ -467,11 +474,18 @@ protected TResponse callPostApi( Response httpResponse = builder.post(Entity.entity(request, JSON_UTF8_TYPE)); + if (httpResponse.getStatusInfo().getFamily() != Response.Status.Family.SUCCESSFUL) + { + throw new WebApplicationException(httpResponse); + } + TResponse apiResponseObject = httpResponse.readEntity(responseClass); - if (apiResponseObject instanceof ApiResponse) { + if (apiResponseObject instanceof ApiResponse) + { ((ApiResponse) apiResponseObject).setResponseHeaders(httpResponse.getStringHeaders()); } + return apiResponseObject; } diff --git a/src/test/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImplTest.java b/src/test/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImplTest.java new file mode 100644 index 00000000..d768834d --- /dev/null +++ b/src/test/java/com/authlete/jaxrs/api/AuthleteApiJaxrsImplTest.java @@ -0,0 +1,258 @@ +package com.authlete.jaxrs.api; + + +import com.authlete.common.api.AuthleteApi; +import com.authlete.common.api.AuthleteApiException; +import com.authlete.common.api.Options; +import com.authlete.common.conf.AuthleteConfiguration; +import com.authlete.common.dto.AuthorizationRequest; +import com.authlete.common.dto.AuthorizationResponse; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.Response; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +/** + * Tests for {@link AuthleteApiJaxrsImpl} to verify that non-2xx HTTP + * responses from the Authlete API are correctly raised as + * {@link AuthleteApiException} with the appropriate status code. + * + *

+ * The JAX-RS typed overloads ({@code builder.get(Class)}, + * {@code builder.post(Entity, Class)}) throw + * {@code WebApplicationException} on non-2xx responses, whereas the + * untyped overloads ({@code builder.get()}, {@code builder.post(Entity)}) + * return the {@code Response} silently. These tests ensure that the + * explicit status check is in place so that the error-handling contract + * is preserved regardless of which overload is used internally. + *

+ */ +public class AuthleteApiJaxrsImplTest +{ + private static class TestHarness + { + final Invocation.Builder builder; + final AuthleteApi api; + + TestHarness() + { + AuthleteConfiguration configuration = + mock(AuthleteConfiguration.class); + when(configuration.getBaseUrl()).thenReturn("http://example.com"); + when(configuration.getServiceApiKey()).thenReturn("key"); + when(configuration.getServiceApiSecret()).thenReturn("secret"); + when(configuration.getApiVersion()).thenReturn("V2"); + + Client client = mock(Client.class); + WebTarget webTarget = mock(WebTarget.class); + builder = mock(Invocation.Builder.class); + + ClientBuilder clientBuilder = mock(ClientBuilder.class); + doReturn(client).when(clientBuilder).build(); + doReturn(webTarget).when(client).target(anyString()); + doReturn(webTarget).when(webTarget).path(anyString()); + doReturn(webTarget).when(webTarget) + .queryParam(anyString(), any()); + doReturn(builder).when(webTarget).request(); + doReturn(builder).when(webTarget) + .request(any(MediaType.class)); + doReturn(builder).when(builder).header(anyString(), any()); + + AuthleteApiImpl impl = new AuthleteApiImpl(configuration); + impl.setJaxRsClientBuilder(clientBuilder); + api = impl; + } + } + + + private static Response createErrorResponse(int statusCode) + { + Response response = mock(Response.class); + Response.StatusType statusType = mock(Response.StatusType.class); + + doReturn(statusCode).when(statusType).getStatusCode(); + doReturn("Error").when(statusType).getReasonPhrase(); + doReturn(Response.Status.Family.familyOf(statusCode)) + .when(statusType).getFamily(); + + doReturn(statusType).when(response).getStatusInfo(); + doReturn(statusCode).when(response).getStatus(); + doReturn(true).when(response).hasEntity(); + doReturn("{\"resultCode\":\"error\"}") + .when(response).readEntity(String.class); + doReturn(new MultivaluedHashMap()) + .when(response).getStringHeaders(); + + return response; + } + + + private static Response createSuccessResponse() + { + Response response = mock(Response.class); + Response.StatusType statusType = mock(Response.StatusType.class); + + doReturn(Response.Status.Family.SUCCESSFUL) + .when(statusType).getFamily(); + doReturn(statusType).when(response).getStatusInfo(); + doReturn(new MultivaluedHashMap()) + .when(response).getStringHeaders(); + + return response; + } + + + // --------------------------------------------------------------- + // POST: non-2xx should throw AuthleteApiException + // --------------------------------------------------------------- + + @Test + public void testPostApiThrowsOn400() + { + TestHarness h = new TestHarness(); + Response response = createErrorResponse(400); + doReturn(response).when(h.builder).post(any()); + + AuthleteApiException ex = assertThrows( + AuthleteApiException.class, + () -> h.api.authorization( + new AuthorizationRequest(), new Options())); + + assertEquals(400, ex.getStatusCode()); + } + + @Test + public void testPostApiThrowsOn401() + { + TestHarness h = new TestHarness(); + Response response = createErrorResponse(401); + doReturn(response).when(h.builder).post(any()); + + AuthleteApiException ex = assertThrows( + AuthleteApiException.class, + () -> h.api.authorization( + new AuthorizationRequest(), new Options())); + + assertEquals(401, ex.getStatusCode()); + } + + @Test + public void testPostApiThrowsOn500() + { + TestHarness h = new TestHarness(); + Response response = createErrorResponse(500); + doReturn(response).when(h.builder).post(any()); + + AuthleteApiException ex = assertThrows( + AuthleteApiException.class, + () -> h.api.authorization( + new AuthorizationRequest(), new Options())); + + assertEquals(500, ex.getStatusCode()); + } + + + // --------------------------------------------------------------- + // GET: non-2xx should throw AuthleteApiException + // --------------------------------------------------------------- + + @Test + public void testGetApiThrowsOn400() + { + TestHarness h = new TestHarness(); + Response response = createErrorResponse(400); + doReturn(response).when(h.builder).get(); + + AuthleteApiException ex = assertThrows( + AuthleteApiException.class, + () -> h.api.getServiceJwks(new Options())); + + assertEquals(400, ex.getStatusCode()); + } + + @Test + public void testGetApiThrowsOn401() + { + TestHarness h = new TestHarness(); + Response response = createErrorResponse(401); + doReturn(response).when(h.builder).get(); + + AuthleteApiException ex = assertThrows( + AuthleteApiException.class, + () -> h.api.getServiceJwks(new Options())); + + assertEquals(401, ex.getStatusCode()); + } + + @Test + public void testGetApiThrowsOn500() + { + TestHarness h = new TestHarness(); + Response response = createErrorResponse(500); + doReturn(response).when(h.builder).get(); + + AuthleteApiException ex = assertThrows( + AuthleteApiException.class, + () -> h.api.getServiceJwks(new Options())); + + assertEquals(500, ex.getStatusCode()); + } + + + // --------------------------------------------------------------- + // 2xx success paths + // --------------------------------------------------------------- + + @Test + public void testPostApiSucceedsOn200() + { + TestHarness h = new TestHarness(); + Response response = createSuccessResponse(); + doReturn(new AuthorizationResponse()) + .when(response).readEntity(AuthorizationResponse.class); + doReturn(response).when(h.builder).post(any()); + + AuthorizationResponse result = h.api.authorization( + new AuthorizationRequest(), new Options()); + + assertNotNull(result); + } + + @Test + public void testGetApiSucceedsOn200() + { + TestHarness h = new TestHarness(); + Response response = createSuccessResponse(); + doReturn("{\"keys\":[]}").when(response).readEntity(String.class); + doReturn(response).when(h.builder).get(); + + String result = h.api.getServiceJwks(new Options()); + + assertNotNull(result); + } + + @Test + public void testDeleteApiSucceeds() + { + TestHarness h = new TestHarness(); + Response response = mock(Response.class); + doReturn(200).when(response).getStatus(); + doReturn(response).when(h.builder).delete(); + + h.api.deleteClient("123", new Options()); + } +} From ad884295562fc5b5b20d9242dd03deb83d8eb410 Mon Sep 17 00:00:00 2001 From: meysam Date: Thu, 5 Mar 2026 10:03:06 +0400 Subject: [PATCH 2/2] docs: add changelog entries for v2.91 error handling fix --- CHANGES.ja.md | 14 ++++++++++++++ CHANGES.md | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/CHANGES.ja.md b/CHANGES.ja.md index d9dbb7d1..3ef38bcb 100644 --- a/CHANGES.ja.md +++ b/CHANGES.ja.md @@ -1,6 +1,20 @@ 変更点 ====== +2.91 (2026-03-05) +----------------- + +- `AuthleteApiJaxrsImpl` クラス + * `callGetApi()` と `callPostApi()` で非 2xx HTTP レスポンスが + `AuthleteApiException` をスローしない問題を修正。v2.88 で型付き + JAX-RS オーバーロードから非型付きオーバーロードへ切り替えた際の + エラーハンドリング契約を復元するため、明示的なステータスチェックを追加。 + +- 新しいテスト + * `AuthleteApiJaxrsImplTest` クラス + - GET、POST、DELETE のエラーハンドリングに関するリグレッションテストを追加。 + + 2.90 (2025-12-29) ----------------- diff --git a/CHANGES.md b/CHANGES.md index 9ebd0b52..20e5adb2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,20 @@ CHANGES ======= +2.91 (2026-03-05) +----------------- + +- `AuthleteApiJaxrsImpl` class + * Fixed non-2xx HTTP responses not throwing `AuthleteApiException` in + `callGetApi()` and `callPostApi()`. Added explicit status check to + restore the error-handling contract after the switch from typed to + untyped JAX-RS overloads in v2.88. + +- New test + * `AuthleteApiJaxrsImplTest` class + - Added regression tests for GET, POST, and DELETE error handling. + + 2.90 (2025-12-29) -----------------