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

Open
#8,532 0 comments 0 reactions 0 assignees View on GitHub
triage
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.