Skip to content

Decouple request sending and response deserialization for login (RequestAgent + ResponseDeserializer)#3

Open
koder95 wants to merge 4 commits intocodex/split-requesthandler-into-three-interfacesfrom
codex/split-requesthandler-into-three-interfaces-r8tv3n
Open

Decouple request sending and response deserialization for login (RequestAgent + ResponseDeserializer)#3
koder95 wants to merge 4 commits intocodex/split-requesthandler-into-three-interfacesfrom
codex/split-requesthandler-into-three-interfaces-r8tv3n

Conversation

@koder95
Copy link
Copy Markdown
Contributor

@koder95 koder95 commented Mar 26, 2026

Motivation

  • Decouple request transport from response parsing to allow richer response handling and clearer error mapping in the login flow.
  • Surface deserialization failures as a dedicated runtime exception and ensure AuthorizationHolder is cleared on error.

Description

  • Replaced RequestHandler with a RequestAgent that returns a ResponseProxy, and added ResponseDeserializer and ResponseProxy interfaces to separate sending and parsing responsibilities.
  • Added a domain Response model and a DeserializationException to represent and propagate deserialization failures explicitly.
  • Updated LoginServiceImpl to use RequestAgent + ResponseDeserializer, to extract response body via ResponseProxy, and to map unchecked deserialization errors to DeserializationException while clearing authorization on failure.
  • Removed RequestHandler and updated the README.md to reflect the new util interfaces; tests were updated to exercise the new contracts and error paths.

Testing

  • Updated and expanded LoginServiceImplTest with cases including successful login (shouldLoginSuccessfully), agent-thrown exception propagation (shouldPropagateAgentException), null-deserializer result handling (shouldThrowWhenDeserializerReturnsNull), mapping unchecked deserializer errors to DeserializationException (shouldMapUncheckedDeserializerException), parameterized invalid username/password tests, and logout behavior test (shouldClearAuthorizationOnLogout).
  • Ran the unit test suite with mvn test, and all tests passed.

Codex Task

Summary by CodeRabbit

  • New Features

    • Improved error handling for response deserialization during login
  • Architecture

    • Refined request/response abstraction to make login request processing more robust
  • Documentation

    • Updated README to clarify utility-layer components
  • Tests

    • Expanded LoginService tests covering success, deserialization failures, and input edge cases

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de7c7a21-6736-4703-be24-35b07775224d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR refactors request-response abstractions: removes RequestHandler<T,R> and introduces RequestAgent<T>, ResponseProxy, ResponseDeserializer, a Response domain model, and DeserializationException. LoginServiceImpl is updated to use the new abstractions and tests are adjusted accordingly; README diagram updated.

Changes

Cohort / File(s) Summary
Core Abstraction Layer
src/main/java/pl/vtt/wpi/core/application/util/RequestAgent.java, src/main/java/pl/vtt/wpi/core/application/util/ResponseDeserializer.java, src/main/java/pl/vtt/wpi/core/application/util/ResponseProxy.java
Added new interfaces: RequestAgent<T> (sends Request<T> and returns ResponseProxy), ResponseDeserializer (deserializes response body to typed object), and ResponseProxy (exposes Response).
Removed Abstraction
src/main/java/pl/vtt/wpi/core/application/util/RequestHandler.java
Deleted RequestHandler<T,R> interface formerly responsible for handling requests and returning typed responses.
Domain Model
src/main/java/pl/vtt/wpi/core/domain/model/Response.java
New immutable Response class with body and exception fields; body() throws stored exception or returns body.
Exception Handling
src/main/java/pl/vtt/wpi/core/application/exception/DeserializationException.java
New runtime DeserializationException(String message, Throwable cause) used to wrap deserialization failures.
Service Implementation
src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java
Constructors and logic updated to use RequestAgent<Void> and ResponseDeserializer; obtains Response via ResponseProxy, deserializes into Credentials, and wraps deserialization errors in DeserializationException.
Test Suite
src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java
Refactored/expanded tests: successful login assertions, parameterized invalid input cases, explicit deserialization-failure scenarios, and consolidated test setup.
Documentation
README.md
Architecture diagram text updated to list RequestAgent, ResponseProxy, and ResponseDeserializer inside util package.

Sequence Diagram(s)

sequenceDiagram
    participant LoginService as LoginServiceImpl
    participant Agent as RequestAgent
    participant Proxy as ResponseProxy
    participant Deser as ResponseDeserializer
    participant Domain as Response

    LoginService->>Agent: send(Request<Void>)
    Agent-->>Proxy: returns ResponseProxy
    Proxy->>Domain: getResponse()
    Domain-->>Proxy: Response(body/exception)
    LoginService->>Proxy: proxy.getResponse().body()
    Note right of LoginService: if body() throws -> propagate
    LoginService->>Deser: deserialize(body, Credentials.class)
    Deser-->>LoginService: Credentials or throws
    alt deserialization throws
        LoginService-->>LoginService: wrap in DeserializationException
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Login service implementation #1 — Directly related: earlier implementation using RequestHandler and AuthorizationHolder that this PR refactors into RequestAgent/ResponseDeserializer/Response.

Poem

🐰 Hopping through lines of code I prance,
I send a request and watch responses dance.
Agent brings a proxy, deserializer sings,
Body becomes credentials — oh what joy it brings!
A tiny rabbit cheers this clean expanse.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main architectural change: decoupling request sending (RequestAgent) from response deserialization (ResponseDeserializer) in the login service.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)

✅ Unit Test PR creation complete.
✅ Unit Test PR creation complete.

  • Create PR with unit tests
  • Commit unit tests in branch codex/split-requesthandler-into-three-interfaces-r8tv3n

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbda7931ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
} catch (IncorrectUsernameOrPasswordException e) {
throw e;
} catch (Exception e) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-throw DeserializationException instead of generic wrapper

The generic catch (Exception e) in getCredentials also catches the DeserializationException created just above, so a deserializer failure is surfaced as RuntimeException("Login failed", ...) instead of DeserializationException. This breaks the new contract that deserialization errors are reported distinctly, and callers/tests that handle DeserializationException will never see it when responseDeserializer.deserialize(...) fails.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java (1)

62-82: ⚠️ Potential issue | 🔴 Critical

Critical: DeserializationException is caught and re-wrapped by the outer catch block.

The DeserializationException thrown at line 10 (inside the inner catch block) remains within the outer try block's scope. When it propagates, it encounters the outer catch clauses in order: first catch (IncorrectUsernameOrPasswordException) which doesn't match, then catch (Exception) at line 14 which matches and re-wraps it as RuntimeException("Login failed", e).

This breaks the intended exception contract where deserialization failures should surface as DeserializationException. The test shouldMapUncheckedDeserializerException() expects DeserializationException.class to be thrown but will receive RuntimeException instead.

Additionally, checked exceptions from ResponseDeserializer.deserialize() (the interface declares throws Exception) bypass the inner catch (RuntimeException) entirely and are also wrapped as RuntimeException("Login failed") rather than DeserializationException.

Fix: Add DeserializationException to the outer catch-and-rethrow clause:

-    } catch (IncorrectUsernameOrPasswordException e) {
+    } catch (IncorrectUsernameOrPasswordException | DeserializationException e) {
         throw e;
     } catch (Exception e) {

For complete handling of checked exceptions, also update the inner catch:

-        } catch (RuntimeException e) {
+        } catch (Exception e) {
             throw new DeserializationException("Failed to deserialize login response", e);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java`
around lines 62 - 82, The getCredentials method wraps DeserializationException
thrown from responseDeserializer.deserialize into a RuntimeException via the
outer catch; change the inner catch to catch Exception (not just
RuntimeException) and rethrow as DeserializationException when deserialize(...)
fails, and add an outer catch clause for DeserializationException (placed before
the generic catch) that simply rethrows it so it won't be wrapped by the catch
(Exception) block; reference methods/classes: getCredentials,
responseDeserializer.deserialize, DeserializationException, requestAgent,
requestFactory.create, ResponseProxy, Credentials.
🧹 Nitpick comments (3)
src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java (1)

28-35: Consider documenting the deprecation reason or removal timeline.

The @Deprecated(forRemoval = true) annotation signals intent to remove this constructor, but there's no @deprecated Javadoc explaining why or what to use instead. This helps consumers migrate before removal.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java`
around lines 28 - 35, Add a Javadoc block to the deprecated constructor in
LoginServiceImpl explaining why it's deprecated, when it will be removed (or the
planned timeline), and what to use instead (e.g., an alternative constructor or
factory/method). Update the constructor's JavaDoc to include an `@deprecated` tag
with a short migration note referencing the preferred mechanism (for example,
the non-deprecated constructor or a builder/factory), so callers know how to
migrate before removal.
src/main/java/pl/vtt/wpi/core/domain/model/Response.java (1)

7-10: Consider enforcing the either-body-or-exception invariant in the constructor.

The Response class conceptually represents a result that is either a successful body or an error. Currently, the constructor permits invalid states: both body and exception being non-null, or both being null. While the context notes that implementations "must ensure" proper usage, adding a constructor check would enforce this contract and prevent subtle bugs.

♻️ Optional: Add constructor validation
 public Response(String body, Exception exception) {
+    if (body != null && exception != null) {
+        throw new IllegalArgumentException("Response cannot have both body and exception");
+    }
     this.body = body;
     this.exception = exception;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/pl/vtt/wpi/core/domain/model/Response.java` around lines 7 -
10, The Response constructor currently allows both body and exception to be null
or both non-null; update the Response(String body, Exception exception)
constructor to validate that exactly one of the two is non-null (i.e., body ==
null XOR exception == null) and throw an IllegalArgumentException with a clear
message if the invariant is violated, then assign the fields as before;
reference: Response constructor and the body and exception fields.
src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java (1)

125-133: Test relies on implementation detail that logout() doesn't use dependencies.

Passing all null dependencies works because logout() currently only calls AuthorizationHolder.clear(). If logout() ever needs to make a server call or use other dependencies, this test would fail with NPE rather than a meaningful assertion failure.

♻️ Optional: Use a valid instance for logout test
 `@Test`
 `@DisplayName`("Should clear authorization on logout")
 void shouldClearAuthorizationOnLogout() {
-    LoginServiceImpl instance = new LoginServiceImpl(null, null, null, null);
+    LoginServiceImpl instance = instanceWithSuccessResponse(new Credentials(USERNAME, TOKEN));
     AuthorizationHolder.authorize("Basic", "seeded-token");
     assertNotNull(AuthorizationHolder.get());

     instance.logout();

     assertNull(AuthorizationHolder.get());
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java`
around lines 125 - 133, The test should not pass nulls to LoginServiceImpl
because it relies on the implementation detail that logout() ignores
dependencies; instead construct LoginServiceImpl with valid/mocked dependencies
so a future change that uses them won't cause NPEs—replace the null arguments in
the test with Mockito.mock(...) (or lightweight real instances) for the
constructor parameters used by LoginServiceImpl, keep using
AuthorizationHolder.authorize("Basic","seeded-token") and then call
instance.logout() and assertNull(AuthorizationHolder.get()) to verify clearing
behavior; target symbols: LoginServiceImpl constructor, logout(), and
AuthorizationHolder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java`:
- Around line 62-82: The getCredentials method wraps DeserializationException
thrown from responseDeserializer.deserialize into a RuntimeException via the
outer catch; change the inner catch to catch Exception (not just
RuntimeException) and rethrow as DeserializationException when deserialize(...)
fails, and add an outer catch clause for DeserializationException (placed before
the generic catch) that simply rethrows it so it won't be wrapped by the catch
(Exception) block; reference methods/classes: getCredentials,
responseDeserializer.deserialize, DeserializationException, requestAgent,
requestFactory.create, ResponseProxy, Credentials.

---

Nitpick comments:
In
`@src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java`:
- Around line 28-35: Add a Javadoc block to the deprecated constructor in
LoginServiceImpl explaining why it's deprecated, when it will be removed (or the
planned timeline), and what to use instead (e.g., an alternative constructor or
factory/method). Update the constructor's JavaDoc to include an `@deprecated` tag
with a short migration note referencing the preferred mechanism (for example,
the non-deprecated constructor or a builder/factory), so callers know how to
migrate before removal.

In `@src/main/java/pl/vtt/wpi/core/domain/model/Response.java`:
- Around line 7-10: The Response constructor currently allows both body and
exception to be null or both non-null; update the Response(String body,
Exception exception) constructor to validate that exactly one of the two is
non-null (i.e., body == null XOR exception == null) and throw an
IllegalArgumentException with a clear message if the invariant is violated, then
assign the fields as before; reference: Response constructor and the body and
exception fields.

In
`@src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java`:
- Around line 125-133: The test should not pass nulls to LoginServiceImpl
because it relies on the implementation detail that logout() ignores
dependencies; instead construct LoginServiceImpl with valid/mocked dependencies
so a future change that uses them won't cause NPEs—replace the null arguments in
the test with Mockito.mock(...) (or lightweight real instances) for the
constructor parameters used by LoginServiceImpl, keep using
AuthorizationHolder.authorize("Basic","seeded-token") and then call
instance.logout() and assertNull(AuthorizationHolder.get()) to verify clearing
behavior; target symbols: LoginServiceImpl constructor, logout(), and
AuthorizationHolder.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f10a14d2-8b28-4d22-898c-c6fff540587b

📥 Commits

Reviewing files that changed from the base of the PR and between dc50110 and dbda793.

📒 Files selected for processing (9)
  • README.md
  • src/main/java/pl/vtt/wpi/core/application/exception/DeserializationException.java
  • src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java
  • src/main/java/pl/vtt/wpi/core/application/util/RequestAgent.java
  • src/main/java/pl/vtt/wpi/core/application/util/RequestHandler.java
  • src/main/java/pl/vtt/wpi/core/application/util/ResponseDeserializer.java
  • src/main/java/pl/vtt/wpi/core/application/util/ResponseProxy.java
  • src/main/java/pl/vtt/wpi/core/domain/model/Response.java
  • src/test/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImplTest.java
💤 Files with no reviewable changes (1)
  • src/main/java/pl/vtt/wpi/core/application/util/RequestHandler.java

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java`:
- Around line 68-72: The catch only handles RuntimeException so checked
exceptions from responseDeserializer.deserialize(...) escape your intended
DeserializationException; update the try/catch in LoginServiceImpl around
responseDeserializer.deserialize(responseBodyString, Credentials.class) to catch
Exception (or Throwable as appropriate), and if the caught exception is already
a DeserializationException rethrow it as-is, otherwise wrap it in a new
DeserializationException("Failed to deserialize login response", e) before
throwing; ensure no other generic RuntimeException wrapping at line 76 masks the
original cause.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db479b7f-ad73-4cbe-822c-29b0caee1e23

📥 Commits

Reviewing files that changed from the base of the PR and between dbda793 and c3f9981.

📒 Files selected for processing (1)
  • src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java

Comment on lines +68 to +72
try {
responseBody = responseDeserializer.deserialize(responseBodyString, Credentials.class);
} catch (RuntimeException e) {
throw new DeserializationException("Failed to deserialize login response", e);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Checked exceptions from deserialize() escape DeserializationException wrapping.

The ResponseDeserializer.deserialize() interface declares throws Exception (checked), but this catch block only handles RuntimeException. Checked exceptions (e.g., IOException from JSON parsing) will bypass this block and be wrapped as generic RuntimeException("Login failed", e) at line 76 instead of DeserializationException.

To properly surface all deserialization failures as DeserializationException and also preserve any DeserializationException already thrown by the deserializer (avoiding double-wrapping):

🐛 Proposed fix
             try {
                 responseBody = responseDeserializer.deserialize(responseBodyString, Credentials.class);
-            } catch (RuntimeException e) {
+            } catch (DeserializationException e) {
+                throw e;
+            } catch (Exception e) {
                 throw new DeserializationException("Failed to deserialize login response", e);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
responseBody = responseDeserializer.deserialize(responseBodyString, Credentials.class);
} catch (RuntimeException e) {
throw new DeserializationException("Failed to deserialize login response", e);
}
try {
responseBody = responseDeserializer.deserialize(responseBodyString, Credentials.class);
} catch (DeserializationException e) {
throw e;
} catch (Exception e) {
throw new DeserializationException("Failed to deserialize login response", e);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/pl/vtt/wpi/core/application/service/impl/LoginServiceImpl.java`
around lines 68 - 72, The catch only handles RuntimeException so checked
exceptions from responseDeserializer.deserialize(...) escape your intended
DeserializationException; update the try/catch in LoginServiceImpl around
responseDeserializer.deserialize(responseBodyString, Credentials.class) to catch
Exception (or Throwable as appropriate), and if the caught exception is already
a DeserializationException rethrow it as-is, otherwise wrap it in a new
DeserializationException("Failed to deserialize login response", e) before
throwing; ensure no other generic RuntimeException wrapping at line 76 masks the
original cause.

@koder95 koder95 changed the base branch from main to codex/split-requesthandler-into-three-interfaces March 26, 2026 19:29
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

14 UNAVAILABLE: read ECONNRESET

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

✅ Created PR with unit tests: #4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant