Skip to content

Comments

Replaced IOException during body access with custom ReadingBodyException & WritingBodyException#2800

Open
rrayst wants to merge 1 commit into6.Xfrom
reading-body-exception
Open

Replaced IOException during body access with custom ReadingBodyException & WritingBodyException#2800
rrayst wants to merge 1 commit into6.Xfrom
reading-body-exception

Conversation

@rrayst
Copy link
Member

@rrayst rrayst commented Feb 17, 2026

…ingBodyExceptionandWritingBodyException` across body handling methods.

Summary by CodeRabbit

Release Notes

Refactoring

  • Simplified HTTP body operation exception handling by removing checked exceptions from public method signatures. Introduced custom runtime exceptions for reading and writing body content, providing improved error clarity and cleaner API usage.
  • Unified error handling across interceptors and utilities to consistently handle the new exception types throughout the system.

Tests

  • Added architectural testing to enforce consistent exception handling patterns in HTTP body operations.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 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.

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

This pull request systematically converts checked IOException exceptions to unchecked exceptions (ReadingBodyException and WritingBodyException) throughout the HTTP body handling and related code. Public methods in AbstractBody and its subclasses no longer declare IOException; instead, IOExceptions are wrapped into runtime exceptions. Callers across interceptors and utilities are updated to handle the new exception types.

Changes

Cohort / File(s) Summary
HTTP Body Exception Classes
core/src/main/java/com/predic8/membrane/core/http/WritingBodyException.java
New unchecked exception class for write errors; extends RuntimeException with constructor accepting underlying Exception.
HTTP Body Base Classes
core/src/main/java/com/predic8/membrane/core/http/AbstractBody.java, core/src/main/java/com/predic8/membrane/core/http/Body.java
Remove IOException from public method signatures (read, write, discard, getLength, getRaw, getContent, getContentAsStream, writeStreamed); wrap IOExceptions into ReadingBodyException and WritingBodyException; update toString() error handling.
HTTP Body Implementations
core/src/main/java/com/predic8/membrane/core/http/ChunkedBody.java, core/src/main/java/com/predic8/membrane/core/http/EmptyBody.java
Remove IOException from read, write, discard, getLength, writeStreamed signatures; wrap IOExceptions into ReadingBodyException and WritingBodyException in try-catch blocks.
Message & Observer Classes
core/src/main/java/com/predic8/membrane/core/http/Message.java, core/src/main/java/com/predic8/membrane/core/http/BodyCollectingMessageObserver.java
Update getBodyAsStream, getBodyAsStreamDecoded, getBodyAsStringDecoded, setBody, setBodyContent, isBodyEmpty to throw ReadingBodyException instead of IOException; update observer exception handling.
HTTP Exchange & Store
core/src/main/java/com/predic8/membrane/core/exchange/AbstractExchange.java, core/src/main/java/com/predic8/membrane/core/exchangestore/FileExchangeStore.java
Remove try-catch blocks around getLength() and snapInternal() calls; exception handling now relies on unchecked exceptions from body operations.
HTTP Transport & Protocol
core/src/main/java/com/predic8/membrane/core/transport/http/HttpServerHandler.java, core/src/main/java/com/predic8/membrane/core/transport/http2/StreamInfo.java
Update removeBodyFromBuffer and Http2Body.writeStreamed to declare ReadingBodyException; wrap discardBody and IO operations in try-catch to convert IOException to appropriate unchecked exceptions.
Utility & Helper Classes
core/src/main/java/com/predic8/membrane/core/util/MessageUtil.java, core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java, core/src/main/java/com/predic8/membrane/core/lang/spel/typeconverters/SpELBodyToStringTypeConverter.java
Remove IOException from method signatures; wrap IOExceptions in try-catch blocks to throw ReadingBodyException; update error handling for body access operations.
OpenAPI Utilities
core/src/main/java/com/predic8/membrane/core/openapi/util/Utils.java
Remove IOException from getOpenapiValidatorRequest and getOpenapiValidatorResponse signatures; populate request/response with headers and body when present.
Interceptors
core/src/main/java/com/predic8/membrane/core/interceptor/balancer/BalancerHealthMonitor.java, core/src/main/java/com/predic8/membrane/core/interceptor/beautifier/BeautifierInterceptor.java, core/src/main/java/com/predic8/membrane/core/interceptor/log/LogInterceptor.java, core/src/main/java/com/predic8/membrane/core/interceptor/log/access/AccessLogInterceptorService.java, core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/NtlmInterceptor.java, core/src/main/java/com/predic8/membrane/core/interceptor/rest/XML2HTTP.java, core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java, core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java, core/src/main/java/com/predic8/membrane/core/interceptor/flow/CallInterceptor.java
Update catch blocks to handle ReadingBodyException instead of IOException; adjust exception handling and logging; remove defensive try-catch blocks around body operations where IOExceptions no longer thrown.
Statistics Collection
core/src/main/java/com/predic8/membrane/core/proxies/StatisticCollector.java
Update exception type from IOException to ReadingBodyException in catch block; improve log message with exception details.
Test Classes
core/src/test/java/com/predic8/membrane/core/interceptor/BodyDoesntThrowIOExceptionTest.java, core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/testinterceptors/AbortFlowTestInterceptor.java, core/src/test/java/com/predic8/membrane/core/interceptor/flow/invocation/testinterceptors/ExceptionTestInterceptor.java, core/src/test/java/com/predic8/membrane/core/transport/ssl/SessionResumptionTest.java
Add ArchUnit test to enforce AbstractBody methods do not throw IOException; remove try-catch blocks around body access in test interceptors relying on unchecked exceptions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • predic8
  • christiangoerdes

Poem

🐰 Hops through code with joy so bright,
IOException takes its final flight,
RuntimeException now takes the stage,
Unchecked exceptions turn the page,
Bodies read without a frown,
Errors bubble gently down!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% 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 reflects the main change: replacing IOException with two custom exceptions (ReadingBodyException and WritingBodyException) across body handling.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch reading-body-exception

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@rrayst
Copy link
Member Author

rrayst commented Feb 17, 2026

/ok-to-test

@rrayst rrayst marked this pull request as ready for review February 18, 2026 04:36
@rrayst
Copy link
Member Author

rrayst commented Feb 18, 2026

@coderabbitai plz review

@rrayst
Copy link
Member Author

rrayst commented Feb 18, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@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: 7

Caution

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

⚠️ Outside diff range comments (6)
core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/NtlmInterceptor.java (1)

78-78: ⚠️ Potential issue | 🟠 Major

Unguarded ReadingBodyException at the handleResponse call site will propagate as an uncaught runtime exception.

The previous code wrapped getContent() in a try/catch that swallowed/logged IOException. Now that the method throws the unchecked ReadingBodyException, the call at line 78 is completely unprotected. If the body read fails, the exception escapes handleResponse() entirely, bypassing NTLM logic and likely resulting in a generic 500 rather than a controlled response.

The call at line 112 (inside authenticate) happens to be safe because the surrounding catch (Exception e) on line 94 catches RuntimeException subtypes — but note the error detail "Could not authenticate!" would be misleading if the actual failure was a body-read error.

🛡️ Proposed fix: guard both call sites appropriately
-    private void prepareStreamByEmptyingIt(Exchange exc) {
-        exc.getResponse().getBody().getContent();
-    }
+    private void prepareStreamByEmptyingIt(Exchange exc) {
+        try {
+            exc.getResponse().getBody().getContent();
+        } catch (ReadingBodyException e) {
+            log.debug("Failed to empty response body stream: {}", e.getMessage(), e);
+        }
+    }

Based on learnings, body reading failures in interceptors should be caught and logged at debug level rather than allowed to propagate, to avoid cluttering error logs with common stream-reading issues while still surfacing them when debug logging is enabled.

Also applies to: 140-142

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

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/NtlmInterceptor.java`
at line 78, handle the unchecked ReadingBodyException thrown when reading the
message body so it doesn't escape handleResponse()/authenticate(): wrap calls to
prepareStreamByEmptyingIt(exc) and any direct getContent() body reads in a
try/catch that catches ReadingBodyException (or RuntimeException if
ReadingBodyException is not importable) and log the failure at debug level
(using the class logger) instead of letting it propagate; after catching,
proceed with the NTLM logic (e.g., return/continue as existing flow expects) so
authentication paths still run and error messages (like "Could not
authenticate!") remain accurate.
core/src/main/java/com/predic8/membrane/core/interceptor/beautifier/BeautifierInterceptor.java (1)

62-72: ⚠️ Potential issue | 🟠 Major

Catch block does not handle ReadingBodyException thrown by getBodyAsStreamDecoded() and setBodyContent().

The catch block only catches IOException, but:

  • msg.getBodyAsStreamDecoded() throws ReadingBodyException (unchecked)
  • msg.setBodyContent(...) throws ReadingBodyException (unchecked)

Both are runtime exceptions that will propagate unhandled, breaking the "swallow and continue" contract.

Add a catch for ReadingBodyException:

🔧 Proposed fix
-        } catch (IOException e) {
+        } catch (ReadingBodyException e) {
+            // Body I/O failed; beautification not possible – continue as-is
+            log.info("Could not beautify message body: {}", e.getMessage());
+        } catch (IOException e) {
             // If it is not possible to beautify, to nothing
             // Cause will be often user input => do not log stacktrace
             log.info("Could not beautify message body: {}", e.getMessage());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/beautifier/BeautifierInterceptor.java`
around lines 62 - 72, The catch currently only handles IOException but
msg.getBodyAsStreamDecoded() and msg.setBodyContent(...) can throw the unchecked
ReadingBodyException, so update BeautifierInterceptor to also catch
ReadingBodyException (either as a separate catch or combined) around the try
that calls getPrettifier(...), msg.getBodyAsStreamDecoded() and
msg.setBodyContent(...); in that catch log the same info-level message as for
IOException (no stacktrace) and ensure the method still returns CONTINUE so
beautifier failures are swallowed as intended.
core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java (1)

88-95: ⚠️ Potential issue | 🟡 Minor

Wrap body retrieval in try-catch to prevent ReadingBodyException from silently swallowing the MismatchedInputException.

If getBodyAsStringDecoded() (line 89) throws ReadingBodyException, it will escape the current MismatchedInputException handler and be caught by the broad catch (Exception e) at line 96, causing the original exception context to be lost.

Proposed fix
         } catch (MismatchedInputException e) {
-            String body = exchange.getMessage(flow).getBodyAsStringDecoded();
-            if (body == null || body.isEmpty()) {
-                log.info("Error evaluating Jsonpath {}. Body is empty!", expression);
-            } else {
-                log.info("Error evaluating Jsonpath {}. Body is: {}", expression, truncateAfter(body, 200));
+            try {
+                String body = exchange.getMessage(flow).getBodyAsStringDecoded();
+                if (body == null || body.isEmpty()) {
+                    log.info("Error evaluating Jsonpath {}. Body is empty!", expression);
+                } else {
+                    log.info("Error evaluating Jsonpath {}. Body is: {}", expression, truncateAfter(body, 200));
+                }
+            } catch (ReadingBodyException rbe) {
+                log.debug("Could not read body for error context: {}", ExceptionUtil.concatMessageAndCauseMessages(rbe));
             }
             throw new ExchangeExpressionException(expression, e);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java`
around lines 88 - 95, In JsonpathExchangeExpression (inside the catch
(MismatchedInputException e) block) wrap the call to
exchange.getMessage(flow).getBodyAsStringDecoded() in its own try-catch that
catches ReadingBodyException so that a ReadingBodyException thrown while
retrieving the body does not escape and replace the original
MismatchedInputException; if ReadingBodyException occurs, log a message
indicating the body could not be read (including the ReadingBodyException
details) and then continue to throw the ExchangeExpressionException constructed
from the original MismatchedInputException (e) so the original parsing error
context is preserved.
core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java (1)

199-201: ⚠️ Potential issue | 🟡 Minor

analyseSOAPMessage silently swallows ReadingBodyException, inconsistent with isSOAP.

isSOAP (lines 56–57) now explicitly rethrows ReadingBodyException, but analyseSOAPMessage lets the same exception fall into the generic catch (Exception e) and returns NO_SOAP_RESULT. A body-read failure is indistinguishable from a non-SOAP message, potentially causing incorrect routing decisions downstream.

Apply the same rethrow guard used in isSOAP:

🐛 Proposed fix
+        } catch (ReadingBodyException e) {
+            throw e;
         } catch (Exception e) {
             log.warn("Ignoring exception: ", e);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java` around lines
199 - 201, The catch-all in analyseSOAPMessage currently swallows
ReadingBodyException making body-read failures indistinguishable from non-SOAP
results; update analyseSOAPMessage to detect and rethrow ReadingBodyException
just like isSOAP does (i.e., if (e instanceof ReadingBodyException) throw e), so
ReadingBodyException is propagated up instead of returning NO_SOAP_RESULT;
locate the catch(Exception e) in analyseSOAPMessage and add the same rethrow
guard referencing ReadingBodyException and the analyseSOAPMessage method.
core/src/main/java/com/predic8/membrane/core/http/Message.java (2)

136-142: ⚠️ Potential issue | 🟠 Major

getBodyAsStringDecoded() will double-wrap ReadingBodyException.

MessageUtil.getContent(this) can throw ReadingBodyException (unchecked), which the catch (Exception e) on line 139 would intercept and re-wrap into a new ReadingBodyException(e). This creates a redundant wrapper chain.

The sibling method getBodyAsStreamDecoded() (line 116-117) correctly handles this by re-throwing ReadingBodyException before the general catch (Exception e). Apply the same pattern here.

🐛 Proposed fix
 	public String getBodyAsStringDecoded() throws ReadingBodyException {
 		try {
 			return new String(MessageUtil.getContent(this), getCharsetOrDefault());
+		} catch (ReadingBodyException e) {
+			throw e;
 		} catch (Exception e) {
 			throw new ReadingBodyException(e);
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/http/Message.java` around lines
136 - 142, The method getBodyAsStringDecoded currently catches Exception and
always wraps it in a new ReadingBodyException, causing double-wrapping when
MessageUtil.getContent(this) already throws ReadingBodyException; change the
catch handling to first re-throw ReadingBodyException unchanged and only wrap
other Exceptions (mirror the pattern used in getBodyAsStreamDecoded): detect if
the caught exception is an instance of ReadingBodyException and re-throw it,
otherwise wrap the exception in new ReadingBodyException before throwing,
referencing getBodyAsStringDecoded, MessageUtil.getContent(this), and
ReadingBodyException.

397-409: ⚠️ Potential issue | 🟡 Minor

Redundant wrapping: ReadingBodyException is already a RuntimeException.

Line 401-402 catches ReadingBodyException and wraps it in new RuntimeException(e). Since ReadingBodyException already extends RuntimeException, this adds a needless wrapper layer that obscures the original exception type in stack traces.

🐛 Proposed fix — rethrow directly
 		public void bodyComplete(AbstractBody body2) {
 			try {
 				result.setBody(getBody(body2));
 			} catch (ReadingBodyException e) {
-				throw new RuntimeException(e);
+				throw e;
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/http/Message.java` around lines
397 - 409, In bodyComplete (in Message.java) avoid wrapping ReadingBodyException
in a new RuntimeException; since ReadingBodyException already extends
RuntimeException, either remove the try/catch around
result.setBody(getBody(body2)) or rethrow the caught exception directly (throw
e) so the original exception type and stack trace are preserved; locate the
bodyComplete method that calls result.setBody(getBody(body2)) and replace the
wrapping new RuntimeException(e) with a direct rethrow of the
ReadingBodyException.
🧹 Nitpick comments (7)
core/src/main/java/com/predic8/membrane/core/proxies/StatisticCollector.java (1)

94-94: Log level and exception representation don't match the established pattern.

Two issues with the updated log call:

  1. Root cause silently droppede.getMessage() only surfaces the ReadingBodyException message; if it wraps an underlying IOException (the typical conversion pattern), the root cause is swallowed entirely.
  2. warn level is too loud — body-read failures during statistics counting are likely common/benign. The repo's established pattern is log.debug() for these cases.
💡 Proposed fix aligning with the established pattern
-		} catch (ReadingBodyException e) {
-			log.warn("Could not count statistic: {}", e.getMessage());
+		} catch (ReadingBodyException e) {
+			log.debug("Could not count statistic: {}", ExceptionUtil.concatMessageAndCauseMessages(e));
		}

Also add the import if not already present via a wildcard:

import com.predic8.membrane.core.util.ExceptionUtil; // adjust package as needed

Based on learnings: In Java exception handling for body logging, use log.debug() level with ExceptionUtil.concatMessageAndCauseMessages(e) to log body reading failures, preventing clutter in info/warn logs while still providing full error chain details when debug logging is enabled.

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

In `@core/src/main/java/com/predic8/membrane/core/proxies/StatisticCollector.java`
at line 94, In StatisticCollector (the place with the log.warn("Could not count
statistic: {}", e.getMessage())), change the logging to use debug level and
include the full exception chain by calling
ExceptionUtil.concatMessageAndCauseMessages(e) instead of e.getMessage(); also
ensure ExceptionUtil is imported (e.g. import
com.predic8.membrane.core.util.ExceptionUtil) so the debug log reads the full
concatenated message and avoids using warn for benign body-read failures.
core/src/main/java/com/predic8/membrane/core/lang/spel/typeconverters/SpELBodyToStringTypeConverter.java (2)

29-31: Downgrade log level to debug and avoid double-wrapping the unchecked ReadingBodyException.

Two issues with the catch block after this PR's changes:

  1. Log level: log.warn for a body-reading failure clutters operational logs. Based on learnings from PR #1977, log.debug() with ExceptionUtil.concatMessageAndCauseMessages(e) is the established pattern for body-reading failures in this codebase.

  2. Redundant re-wrap: MessageUtil.getContent(...) now throws ReadingBodyException, which is already a RuntimeException. Catching it and re-wrapping with new RuntimeException(e) adds an extra, unhelpful layer to the stack trace. The simplest fix is to let the unchecked exception propagate naturally and remove the try/catch entirely; alternatively, rethrow the caught exception directly.

♻️ Proposed refactor
-        try {
-            return new String(MessageUtil.getContent(body.getMessage()), body.getMessage().getCharsetOrDefault());
-        } catch (Exception e) {
-            log.warn("Cannot log body content", e);
-            throw new RuntimeException(e);
-        }
+        var message = body.getMessage();
+        return new String(MessageUtil.getContent(message), message.getCharsetOrDefault());

If logging on failure is still desired:

         try {
-            return new String(MessageUtil.getContent(body.getMessage()), body.getMessage().getCharsetOrDefault());
-        } catch (Exception e) {
-            log.warn("Cannot log body content", e);
-            throw new RuntimeException(e);
+            var message = body.getMessage();
+            return new String(MessageUtil.getContent(message), message.getCharsetOrDefault());
+        } catch (ReadingBodyException e) {
+            log.debug("Cannot convert body to String: {}", ExceptionUtil.concatMessageAndCauseMessages(e));
+            throw e;
         }

Based on learnings from PR #1977: body reading failures should be logged at log.debug() level using ExceptionUtil.concatMessageAndCauseMessages(e) to avoid cluttering operational logs.

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

In
`@core/src/main/java/com/predic8/membrane/core/lang/spel/typeconverters/SpELBodyToStringTypeConverter.java`
around lines 29 - 31, The catch block in SpELBodyToStringTypeConverter that
catches Exception around MessageUtil.getContent should be changed: remove the
try/catch so the unchecked ReadingBodyException can propagate (do not wrap it in
new RuntimeException), or if you must log, replace log.warn(...) with
log.debug(ExceptionUtil.concatMessageAndCauseMessages(e)) and rethrow the caught
exception directly (throw e); reference MessageUtil.getContent(...),
ReadingBodyException and ExceptionUtil.concatMessageAndCauseMessages(...) when
applying the change.

28-28: Extract body.getMessage() to a local variable to avoid the double call.

body.getMessage() is invoked twice on the same line. A local variable improves readability and avoids any potential double evaluation.

♻️ Proposed refactor
-            return new String(MessageUtil.getContent(body.getMessage()), body.getMessage().getCharsetOrDefault());
+            var message = body.getMessage();
+            return new String(MessageUtil.getContent(message), message.getCharsetOrDefault());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/com/predic8/membrane/core/lang/spel/typeconverters/SpELBodyToStringTypeConverter.java`
at line 28, In SpELBodyToStringTypeConverter (the method that returns the
String), avoid calling body.getMessage() twice: extract body.getMessage() into a
local variable (e.g., Message msg) and then use MessageUtil.getContent(msg) and
msg.getCharsetOrDefault() to build the new String; this improves readability and
prevents potential double evaluation.
core/src/main/java/com/predic8/membrane/core/interceptor/log/access/AccessLogInterceptorService.java (1)

134-142: Redundant inner try/catch — consider removing or adding a debug log.

The inner try { … } catch (ReadingBodyException e) { return defaultValue; } is now redundant: since ReadingBodyException is unchecked, any unhandled instance will propagate up to the enclosing safe(Supplier<Object>) call (lines 148–154), which already catches Exception and returns the same defaultValue. Both paths produce identical results.

If the explicit handling block is kept purely for clarity, consider adding a log.debug() call to preserve visibility into body-read failures without cluttering info logs — consistent with the team's convention for body reading failures.

♻️ Option A — remove the redundant inner try-catch
 private Supplier<Object> getPayLoadSize(Message msg) {
-    return () -> {
-        try {
-            return msg.getBody().getLength();
-        } catch (ReadingBodyException e) {
-            return defaultValue;
-        }
-    };
+    return () -> msg.getBody().getLength();
 }
♻️ Option B — keep explicit catch, add debug log
 private Supplier<Object> getPayLoadSize(Message msg) {
     return () -> {
         try {
             return msg.getBody().getLength();
         } catch (ReadingBodyException e) {
+            log.debug("Could not read body length: {}", ExceptionUtil.concatMessageAndCauseMessages(e));
             return defaultValue;
         }
     };
 }

Based on learnings, the team convention for body-read failures in logging paths is to use log.debug() with ExceptionUtil.concatMessageAndCauseMessages(e) to avoid cluttering info logs while retaining diagnostic detail.

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

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/log/access/AccessLogInterceptorService.java`
around lines 134 - 142, The inner try/catch inside getPayLoadSize is redundant
because ReadingBodyException is unchecked and safe(Supplier<Object>) already
catches exceptions; either remove the inner try/catch entirely from
getPayLoadSize (leaving it to return msg.getBody().getLength() directly) or, if
you want to keep explicit handling for clarity, add a debug log in the catch
that uses log.debug(ExceptionUtil.concatMessageAndCauseMessages(e)) before
returning defaultValue so body-read failures remain visible at debug level;
update the Supplier<Object> returned by getPayLoadSize accordingly.
core/src/main/java/com/predic8/membrane/core/interceptor/flow/CallInterceptor.java (1)

123-128: ReadingBodyException from setRequestBody escapes handleInternal unhandled — consider catching it.

getNewRequest(exc) is called at line 80, before either try-catch block in handleInternal. If ReadingBodyException is thrown at line 127, it propagates all the way out of handleInternal without an ABORT outcome or a structured error response being set on the exchange.

This gap existed before (a RuntimeException wrapper escaped the same way), so it is not a regression introduced by this PR. That said, transitioning to a typed unchecked exception makes this a natural moment to close it.

♻️ Proposed fix: move request construction inside a guarded block
 private Outcome handleInternal(Exchange exc) {
     final String dest = exchangeExpression.evaluate(exc, REQUEST, String.class);
     log.debug("Calling {}", dest);
 
-    final Exchange newExc = createNewExchange(dest, getNewRequest(exc));
-
-    try (HttpClient client = new HttpClient()) {
+    final Exchange newExc;
+    try {
+        newExc = createNewExchange(dest, getNewRequest(exc));
+    } catch (Exception e) {
+        log.error("Error building request for {}: {}", dest, e.getMessage(), e);
+        internal(router.isProduction(), getDisplayName())
+                .addSubSee("internal-calling")
+                .detail("Internal call")
+                .exception(e)
+                .buildAndSetResponse(exc);
+        return ABORT;
+    }
+
+    try (HttpClient client = new HttpClient()) {
         client.call(newExc);
     } catch (Exception e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/flow/CallInterceptor.java`
around lines 123 - 128, The call to getNewRequest(exc) can throw
ReadingBodyException from setRequestBody and currently escapes handleInternal;
to fix, move the request construction into the guarded try-catch inside
handleInternal (or add a specific catch for ReadingBodyException around
getNewRequest) so the exception is caught, the exchange outcome is set to ABORT,
and an appropriate error response is set on the Exchange; update references to
setRequestBody, getNewRequest, and handleInternal accordingly so
ReadingBodyException is handled before it propagates.
core/src/main/java/com/predic8/membrane/core/http/ChunkedBody.java (1)

227-256: Good separation of read vs. write exceptions, same markAsRead() concern as Body.java.

The nested try-catch structure correctly routes read-path IOExceptions to ReadingBodyException and write-path IOExceptions to WritingBodyException. The comment on line 247 is helpful.

Same note as in Body.java: if either exception is thrown, markAsRead() (line 255) is skipped and observers are never notified. If this is intentional, consider adding a brief comment. If not, consider a finally block for markAsRead().

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

In `@core/src/main/java/com/predic8/membrane/core/http/ChunkedBody.java` around
lines 227 - 256, The method writeStreamed currently skips markAsRead() (and thus
never notifies observers) when a ReadingBodyException or WritingBodyException is
thrown; update writeStreamed (reference method writeStreamed in ChunkedBody) to
ensure markAsRead() always runs by placing it in a finally block (or add a clear
comment if skipping is intentional), and preserve the existing exception mapping
(ReadingBodyException for read IO errors and WritingBodyException for write IO
errors) so behavior of read/write exception handling (readChunkSize,
readByteArray, out.write, out.finish) remains unchanged; also ensure
observers.bodyChunk calls remain inside the read loop.
core/src/main/java/com/predic8/membrane/core/http/Body.java (1)

155-184: Consider ensuring markAsRead() is called even when writing fails.

If WritingBodyException is thrown (lines 172-173 or 180-181), markAsRead() on line 183 is never reached. This means observers are never notified of body completion. While the connection is likely torn down on error, any registered MessageObserver will be left dangling.

If the intent is to always signal completion (even on failure), markAsRead() could be moved into a finally block. If the current behavior is intentional (don't signal on write failure), a brief comment would help clarify.

Also, the negated condition on line 163 is harder to parse than the original while-loop form. A minor readability nit — consider extracting it:

♻️ Readability suggestion for the read loop
-		while (true) {
-            try {
-                if (!((this.length > totalLength || this.length == -1) && (length = inputStream.read(buffer)) > 0))
-                    break;
-            } catch (IOException e) {
-                throw new ReadingBodyException(e);
-            }
+		while (hasMoreToRead(totalLength)) {
+            try {
+                length = inputStream.read(buffer);
+            } catch (IOException e) {
+                throw new ReadingBodyException(e);
+            }
+            if (length <= 0)
+                break;

Where hasMoreToRead is:

private boolean hasMoreToRead(long totalLength) {
    return this.length > totalLength || this.length == -1;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/predic8/membrane/core/http/Body.java` around lines 155
- 184, The writeStreamed method can fail with WritingBodyException before
markAsRead() is reached, leaving observers un-notified; ensure markAsRead() is
always invoked by moving it into a finally block that surrounds the entire
read/write/finish sequence (i.e., wrap the loop, out.finish(null) and associated
try/catches in try { ... } finally { markAsRead(); }) and rethrow any caught
WritingBodyException/ReadingBodyException so behavior remains unchanged, and
optionally simplify the loop condition by extracting the total-length check into
a helper like hasMoreToRead(long totalLength) to improve readability; refer to
writeStreamed, markAsRead, out.finish, WritingBodyException, and
ReadingBodyException when making changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/balancer/BalancerHealthMonitor.java`:
- Around line 138-141: In BalancerHealthMonitor where ReadingBodyException is
caught (catch (ReadingBodyException e) { ... }), replace the current log.debug
call that uses e.getMessage() with log.debug and
ExceptionUtil.concatMessageAndCauseMessages(e) so the full cause chain is
logged; i.e., call log.debug("Calling health endpoint failed: {} {}", exc,
ExceptionUtil.concatMessageAndCauseMessages(e)) to retain deeper cause messages
for diagnostics.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/beautifier/BeautifierInterceptor.java`:
- Around line 57-59: In BeautifierInterceptor.java, inside the catch block that
catches ReadingBodyException (the handler that currently calls log.error("Could
not beautify body ({}), but continuing flow.", e.getMessage()); and returns
CONTINUE), change the logging level from error to debug to follow the project's
convention for body-reading failures; keep the message text and return behavior
unchanged so the catch still returns CONTINUE and only the logging call is
downgraded to log.debug(...).

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/log/LogInterceptor.java`:
- Around line 110-113: Replace the current catch block that calls
writeLog("Error accessing body: " + e.getMessage()) with a DEBUG-level log that
includes the full exception chain: in LogInterceptor's catch
(ReadingBodyException e) use logger.debug("Error accessing body while checking
isBodyEmpty()", e) (or equivalent logger.debug call that passes the exception)
so the message is logged at DEBUG and the stack/cause chain is preserved; ensure
the class imports/uses the existing SLF4J logger instance rather than writeLog.

In `@core/src/main/java/com/predic8/membrane/core/interceptor/rest/XML2HTTP.java`:
- Around line 133-135: The catch in XML2HTTP (method handling unwrap/rewrap
operations) currently only catches XMLStreamException and XML2HTTPException, so
ReadingBodyException and WritingBodyException thrown from calls like
message.getBody().getLength(), message.getBodyAsStreamDecoded(), and
message.setBodyContent() will escape; update the catch to also handle
ReadingBodyException and WritingBodyException and log them (use debug level to
match existing body-failure logging) so the exceptions are swallowed and
recorded as before, or if you intend propagation, add a clear comment in
XML2HTTP explaining callers (e.g., SOAP2RESTInterceptor.unwrapMessageIfNecessary
and REST2SOAPInterceptor.unwrapMessageIfNecessary) must handle these runtime
exceptions.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java`:
- Around line 155-156: In ValidatorInterceptor's catch block for
ReadingBodyException, change the log call to pass the exception as the final
SLF4J argument so the stack trace is preserved; keep the existing message
template (e.g., "Validation failed: {}") but pass e.getMessage() as the
replacement and then pass e (the Throwable) as the last parameter so SLF4J
prints the full stack trace of ReadingBodyException and its cause.

In
`@core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java`:
- Around line 73-75: Replace the error-level logging in
JsonpathExchangeExpression's catch(ReadingBodyException e) block with
debug-level logging and include full cause-chain text: call log.debug(...)
instead of log.error(...) and pass
ExceptionUtil.concatMessageAndCauseMessages(e) as the message/detail; leave the
existing return resultForNoEvaluation(type) behavior unchanged so failures
remain non-fatal.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/BodyDoesntThrowIOExceptionTest.java`:
- Around line 14-19: The ArchRule declaration in BodyDoesntThrowIOExceptionTest
has a typo in the description string and a non-descriptive field name; update
the .as(...) text to "Public instance methods of Body and its subclasses should
not throw IOException." and rename the static field adr to a clearer name like
bodyMethodsShouldNotThrowIOException (update all references) so the rule for
methods declared in classes assignable to AbstractBody that are public,
non-static, should not declare IOException remains correct and readable.

---

Outside diff comments:
In `@core/src/main/java/com/predic8/membrane/core/http/Message.java`:
- Around line 136-142: The method getBodyAsStringDecoded currently catches
Exception and always wraps it in a new ReadingBodyException, causing
double-wrapping when MessageUtil.getContent(this) already throws
ReadingBodyException; change the catch handling to first re-throw
ReadingBodyException unchanged and only wrap other Exceptions (mirror the
pattern used in getBodyAsStreamDecoded): detect if the caught exception is an
instance of ReadingBodyException and re-throw it, otherwise wrap the exception
in new ReadingBodyException before throwing, referencing getBodyAsStringDecoded,
MessageUtil.getContent(this), and ReadingBodyException.
- Around line 397-409: In bodyComplete (in Message.java) avoid wrapping
ReadingBodyException in a new RuntimeException; since ReadingBodyException
already extends RuntimeException, either remove the try/catch around
result.setBody(getBody(body2)) or rethrow the caught exception directly (throw
e) so the original exception type and stack trace are preserved; locate the
bodyComplete method that calls result.setBody(getBody(body2)) and replace the
wrapping new RuntimeException(e) with a direct rethrow of the
ReadingBodyException.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/beautifier/BeautifierInterceptor.java`:
- Around line 62-72: The catch currently only handles IOException but
msg.getBodyAsStreamDecoded() and msg.setBodyContent(...) can throw the unchecked
ReadingBodyException, so update BeautifierInterceptor to also catch
ReadingBodyException (either as a separate catch or combined) around the try
that calls getPrettifier(...), msg.getBodyAsStreamDecoded() and
msg.setBodyContent(...); in that catch log the same info-level message as for
IOException (no stacktrace) and ensure the method still returns CONTINUE so
beautifier failures are swallowed as intended.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/ntlm/NtlmInterceptor.java`:
- Line 78: handle the unchecked ReadingBodyException thrown when reading the
message body so it doesn't escape handleResponse()/authenticate(): wrap calls to
prepareStreamByEmptyingIt(exc) and any direct getContent() body reads in a
try/catch that catches ReadingBodyException (or RuntimeException if
ReadingBodyException is not importable) and log the failure at debug level
(using the class logger) instead of letting it propagate; after catching,
proceed with the NTLM logic (e.g., return/continue as existing flow expects) so
authentication paths still run and error messages (like "Could not
authenticate!") remain accurate.

In
`@core/src/main/java/com/predic8/membrane/core/lang/jsonpath/JsonpathExchangeExpression.java`:
- Around line 88-95: In JsonpathExchangeExpression (inside the catch
(MismatchedInputException e) block) wrap the call to
exchange.getMessage(flow).getBodyAsStringDecoded() in its own try-catch that
catches ReadingBodyException so that a ReadingBodyException thrown while
retrieving the body does not escape and replace the original
MismatchedInputException; if ReadingBodyException occurs, log a message
indicating the body could not be read (including the ReadingBodyException
details) and then continue to throw the ExchangeExpressionException constructed
from the original MismatchedInputException (e) so the original parsing error
context is preserved.

In `@core/src/main/java/com/predic8/membrane/core/util/SOAPUtil.java`:
- Around line 199-201: The catch-all in analyseSOAPMessage currently swallows
ReadingBodyException making body-read failures indistinguishable from non-SOAP
results; update analyseSOAPMessage to detect and rethrow ReadingBodyException
just like isSOAP does (i.e., if (e instanceof ReadingBodyException) throw e), so
ReadingBodyException is propagated up instead of returning NO_SOAP_RESULT;
locate the catch(Exception e) in analyseSOAPMessage and add the same rethrow
guard referencing ReadingBodyException and the analyseSOAPMessage method.

---

Nitpick comments:
In `@core/src/main/java/com/predic8/membrane/core/http/Body.java`:
- Around line 155-184: The writeStreamed method can fail with
WritingBodyException before markAsRead() is reached, leaving observers
un-notified; ensure markAsRead() is always invoked by moving it into a finally
block that surrounds the entire read/write/finish sequence (i.e., wrap the loop,
out.finish(null) and associated try/catches in try { ... } finally {
markAsRead(); }) and rethrow any caught
WritingBodyException/ReadingBodyException so behavior remains unchanged, and
optionally simplify the loop condition by extracting the total-length check into
a helper like hasMoreToRead(long totalLength) to improve readability; refer to
writeStreamed, markAsRead, out.finish, WritingBodyException, and
ReadingBodyException when making changes.

In `@core/src/main/java/com/predic8/membrane/core/http/ChunkedBody.java`:
- Around line 227-256: The method writeStreamed currently skips markAsRead()
(and thus never notifies observers) when a ReadingBodyException or
WritingBodyException is thrown; update writeStreamed (reference method
writeStreamed in ChunkedBody) to ensure markAsRead() always runs by placing it
in a finally block (or add a clear comment if skipping is intentional), and
preserve the existing exception mapping (ReadingBodyException for read IO errors
and WritingBodyException for write IO errors) so behavior of read/write
exception handling (readChunkSize, readByteArray, out.write, out.finish) remains
unchanged; also ensure observers.bodyChunk calls remain inside the read loop.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/flow/CallInterceptor.java`:
- Around line 123-128: The call to getNewRequest(exc) can throw
ReadingBodyException from setRequestBody and currently escapes handleInternal;
to fix, move the request construction into the guarded try-catch inside
handleInternal (or add a specific catch for ReadingBodyException around
getNewRequest) so the exception is caught, the exchange outcome is set to ABORT,
and an appropriate error response is set on the Exchange; update references to
setRequestBody, getNewRequest, and handleInternal accordingly so
ReadingBodyException is handled before it propagates.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/log/access/AccessLogInterceptorService.java`:
- Around line 134-142: The inner try/catch inside getPayLoadSize is redundant
because ReadingBodyException is unchecked and safe(Supplier<Object>) already
catches exceptions; either remove the inner try/catch entirely from
getPayLoadSize (leaving it to return msg.getBody().getLength() directly) or, if
you want to keep explicit handling for clarity, add a debug log in the catch
that uses log.debug(ExceptionUtil.concatMessageAndCauseMessages(e)) before
returning defaultValue so body-read failures remain visible at debug level;
update the Supplier<Object> returned by getPayLoadSize accordingly.

In
`@core/src/main/java/com/predic8/membrane/core/lang/spel/typeconverters/SpELBodyToStringTypeConverter.java`:
- Around line 29-31: The catch block in SpELBodyToStringTypeConverter that
catches Exception around MessageUtil.getContent should be changed: remove the
try/catch so the unchecked ReadingBodyException can propagate (do not wrap it in
new RuntimeException), or if you must log, replace log.warn(...) with
log.debug(ExceptionUtil.concatMessageAndCauseMessages(e)) and rethrow the caught
exception directly (throw e); reference MessageUtil.getContent(...),
ReadingBodyException and ExceptionUtil.concatMessageAndCauseMessages(...) when
applying the change.
- Line 28: In SpELBodyToStringTypeConverter (the method that returns the
String), avoid calling body.getMessage() twice: extract body.getMessage() into a
local variable (e.g., Message msg) and then use MessageUtil.getContent(msg) and
msg.getCharsetOrDefault() to build the new String; this improves readability and
prevents potential double evaluation.

In
`@core/src/main/java/com/predic8/membrane/core/proxies/StatisticCollector.java`:
- Line 94: In StatisticCollector (the place with the log.warn("Could not count
statistic: {}", e.getMessage())), change the logging to use debug level and
include the full exception chain by calling
ExceptionUtil.concatMessageAndCauseMessages(e) instead of e.getMessage(); also
ensure ExceptionUtil is imported (e.g. import
com.predic8.membrane.core.util.ExceptionUtil) so the debug log reads the full
concatenated message and avoids using warn for benign body-read failures.

@rrayst
Copy link
Member Author

rrayst commented Feb 18, 2026

@coderabbitai configuration

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Configuration used: defaults

CodeRabbit configuration
language: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
inheritance: false
reviews:
  profile: chill
  request_changes_workflow: false
  high_level_summary: true
  high_level_summary_instructions: ''
  high_level_summary_placeholder: '@coderabbitai summary'
  high_level_summary_in_walkthrough: false
  auto_title_placeholder: '@coderabbitai'
  auto_title_instructions: ''
  review_status: true
  review_details: false
  commit_status: true
  fail_commit_status: false
  collapse_walkthrough: true
  changed_files_summary: true
  sequence_diagrams: true
  estimate_code_review_effort: true
  assess_linked_issues: true
  related_issues: true
  related_prs: true
  suggested_labels: true
  labeling_instructions: []
  auto_apply_labels: false
  suggested_reviewers: true
  auto_assign_reviewers: false
  in_progress_fortune: true
  poem: true
  enable_prompt_for_ai_agents: true
  path_filters: []
  path_instructions: []
  abort_on_close: true
  disable_cache: false
  auto_review:
    enabled: true
    description_keyword: ''
    auto_incremental_review: true
    auto_pause_after_reviewed_commits: 5
    ignore_title_keywords: []
    labels: []
    drafts: false
    base_branches: []
    ignore_usernames: []
  finishing_touches:
    docstrings:
      enabled: true
    unit_tests:
      enabled: true
    custom: []
  pre_merge_checks:
    docstrings:
      mode: warning
      threshold: 80
    title:
      mode: warning
      requirements: ''
    description:
      mode: warning
    issue_assessment:
      mode: warning
    custom_checks: []
  tools:
    ast-grep:
      rule_dirs: []
      util_dirs: []
      essential_rules: true
      packages: []
    shellcheck:
      enabled: true
    ruff:
      enabled: true
    markdownlint:
      enabled: true
    github-checks:
      enabled: true
      timeout_ms: 90000
    languagetool:
      enabled: true
      enabled_rules: []
      disabled_rules: []
      enabled_categories: []
      disabled_categories: []
      enabled_only: false
      level: default
    biome:
      enabled: true
    hadolint:
      enabled: true
    swiftlint:
      enabled: true
    phpstan:
      enabled: true
      level: default
    phpmd:
      enabled: true
    phpcs:
      enabled: true
    golangci-lint:
      enabled: true
    yamllint:
      enabled: true
    gitleaks:
      enabled: true
    trufflehog:
      enabled: true
    checkov:
      enabled: true
    tflint:
      enabled: true
    detekt:
      enabled: true
    eslint:
      enabled: true
    flake8:
      enabled: true
    fortitudeLint:
      enabled: true
    rubocop:
      enabled: true
    buf:
      enabled: true
    regal:
      enabled: true
    actionlint:
      enabled: true
    pmd:
      enabled: true
    clang:
      enabled: true
    cppcheck:
      enabled: true
    opengrep:
      enabled: true
    semgrep:
      enabled: true
    circleci:
      enabled: true
    clippy:
      enabled: true
    sqlfluff:
      enabled: true
    trivy:
      enabled: true
    prismaLint:
      enabled: true
    pylint:
      enabled: true
    oxc:
      enabled: true
    shopifyThemeCheck:
      enabled: true
    luacheck:
      enabled: true
    brakeman:
      enabled: true
    dotenvLint:
      enabled: true
    htmlhint:
      enabled: true
    stylelint:
      enabled: true
    checkmake:
      enabled: true
    osvScanner:
      enabled: true
    blinter:
      enabled: true
    psscriptanalyzer:
      enabled: true
chat:
  art: true
  auto_reply: true
  integrations:
    jira:
      usage: auto
    linear:
      usage: auto
knowledge_base:
  opt_out: false
  web_search:
    enabled: true
  code_guidelines:
    enabled: true
    filePatterns: []
  learnings:
    scope: auto
  issues:
    scope: auto
  jira:
    usage: auto
    project_keys: []
  linear:
    usage: auto
    team_keys: []
  pull_requests:
    scope: auto
  mcp:
    usage: auto
    disabled_servers: []
code_generation:
  docstrings:
    language: en-US
    path_instructions: []
  unit_tests:
    path_instructions: []
issue_enrichment:
  auto_enrich:
    enabled: false
  planning:
    enabled: true
    auto_planning:
      enabled: true
      labels: []
  labeling:
    labeling_instructions: []
    auto_apply_labels: false

…ingBodyException` and `WritingBodyException` across body handling methods.
@rrayst rrayst force-pushed the reading-body-exception branch from 497ed21 to 5afd227 Compare February 18, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant