ionic-team / ionic-team/capacitor
bug: onConsoleMessage OOMs on low-RAM Android — String.format runs on unbounded console message before any log-level/config gate
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 1.3k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 10
Description
## Bug Report
### Capacitor Version
```
@capacitor/cli: 6.2.1
@capacitor/core: 6.2.1
@capacitor/android: 6.2.1
```
The offending code is unchanged on `main`, so this affects v7 as well.
### Platform(s)
Android (native WebView console bridge). Reproduces on low-RAM devices (e.g. TECNO Camon, ~192 MB WebView heap growth limit).
### Current Behavior
`BridgeWebChromeClient.onConsoleMessage` calls `String.format(...)` on the **entire** JS console message unconditionally, *before* the log-level dispatch (and before any `loggingBehavior` gate). If a `console.log`/`console.error` argument is large — a serialized IndexedDB row, an axios error/response object, etc. — the JS string can be tens or hundreds of MB. Materializing it into a Java `String` blows the WebView heap on low-RAM devices → `java.lang.OutOfMemoryError` → hard crash.
Critically, setting `loggingBehavior: 'none'` / `'production'` does **not** prevent it, because the `String.format` cost is paid before logging is gated.
Current code — `android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java`:
```java
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
String tag = Logger.tags("Console");
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
String msg = String.format( // runs on full, unbounded message
"File: %s - Line %d - Msg: %s",
consoleMessage.sourceId(),
consoleMessage.lineNumber(),
consoleMessage.message() // can be hundreds of MB
);
String level = consoleMessage.messageLevel().name();
// ... level dispatch happens AFTER the format ...
}
return true;
}
```
`isValidMsg` only filters specific string patterns, not size.
In our production app this was the **#2 fatal crash** (~5,300 events over 7 days from our Crashlytics). One affected device attempted a single ~266 MB (`266,032,112`-byte) allocation on every occurrence.
Note: this is a distinct code path from the OOM in #7158 (that one is in Cordova `NativeToJsMessageQueue` / `PluginResult` JSON encoding).
### Expected Behavior
An oversized console message must never crash the app. The bridge should skip formatting when logging is disabled and/or bound the message length before `String.format`.
### Code Reproduction
Minimal — in any Capacitor Android app, run in the WebView on a low-RAM device (or an emulator with a small WebView heap cap):
```js
console.log('x'.repeat(100_000_000)); // ~100M chars
```
App crashes with `java.lang.OutOfMemoryError`. `loggingBehavior: 'none'` in `capacitor.config` does not prevent it.
### Suggested Fix
Two complementary guards in `onConsoleMessage`:
```java
private static final int MAX_CONSOLE_MESSAGE_LENGTH = 8 * 1024; // tunable
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
String tag = Logger.tags("Console");
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
// 1) don't pay the format cost if logging is disabled
if (!Logger.shouldLog()) { // or the equivalent loggingBehavior check
return true;
}
// 2) bound the message so an oversized log can never OOM the bridge
String message = consoleMessage.message();
if (message.length() > MAX_CONSOLE_MESSAGE_LENGTH) {
message = message.substring(0, MAX_CONSOLE_MESSAGE_LENGTH) + "… [truncated]";
}
String msg = String.format(
"File: %s - Line %d - Msg: %s",
consoleMessage.sourceId(), consoleMessage.lineNumber(), message);
// ... existing level dispatch ...
}
return true;
}
```
Happy to open a PR with whatever threshold / config approach the team prefers.
Contributor guide
Research direction
Start in android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java and trace onConsoleMessage through isValidMsg, logging behavior, and level dispatch. Reproduce with console.log('x'.repeat(100_000_000)) on a constrained Android device or emulator; done means oversized messages no longer crash and disabled logging avoids the formatting cost.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, java, javascript
- Domain
- mobile-dev, observability
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100