Skip to content

refactor: unified error handling pipeline with stable cross-platform error codes#606

Merged
sunnylqm merged 3 commits into
masterfrom
refactor/error-handling-pipeline
Jul 7, 2026
Merged

refactor: unified error handling pipeline with stable cross-platform error codes#606
sunnylqm merged 3 commits into
masterfrom
refactor/error-handling-pipeline

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

背景

错误处理与上报专项重构(三步全部包含在本 PR)。此前的问题:错误无机器可读的分类(logger 只能按随 locale/平台漂移的 message 字符串聚合)、错误出口四路并行且互相重叠、关键失败路径静默(switchVersion/markSuccess 失败不上报、native 回滚无日志)、三端 reject 形态与文案不一致。

第 1 步:JS 层单一错误管道(commit 1)

  • 新增 UpdateError extends Error { code, cause, extra } + 稳定错误码集合(src/error.ts);toUpdateError() 对已有 Error 就地附加 code、保持对象身份(afterCheckUpdate 等处的错误同一性不变)
  • client 新增 onError(listener)(返回退订函数)与内部单一出口 emitError()(report 带 code + 通知监听器);report() payload 新增 code 字段(对用户 logger 纯增量)
  • Provider 错误 UI 改为订阅 client.onError:修复默认 throwError:false 下 check/download 错误既不弹窗也不进 lastError 的问题;删除 client/provider 两层重复的 throwError 判定
  • 补报盲区:switchVersion/switchVersionLater 失败 → 新事件 errorSwitchVersionmarkSuccess 失败 → errorMarkSuccess
  • apk 错误保留原始 native 异常(此前无参 catch 丢弃)、改用 i18n 消息(替换裸事件名 Error('errorInstallApk') 等三处)
  • 导出 UpdateError / UpdateErrorCode / UpdateErrorListener;删除死翻译 key error_code

第 2 步:native 可见性与健壮性小改(commit 2,不动接口)

  • 三端回滚补日志:崩溃保护回滚(新版本未 markSuccess)此前三端完全静默,现在 Android Log.e / iOS RCTLogWarn / Harmony console.error 均记录"从 X 回滚到 Y"
  • Android persistEditor commit 失败 release 下也打 Log.e(丢状态写=丢回滚/丢切版本)
  • iOS/Harmony 启动回滚循环补 visited 去重(Android 已有),防状态损坏时死循环
  • iOS:更新失败时删除半成品版本目录(对齐 Android/Harmony,此前泄漏磁盘);下载补 Content-Length 截断校验(对带 Content-Encoding 的响应跳过,避免 NSURLSession 透明解压导致误报)

第 3 步:三端错误码统一(commit 2)

  • 新增 spec 单一来源 cpp/patch_core/error_codes.h(9 个 native 码),iOS 直接 include,Java 镜像 ErrorCodes.java,JS UpdateErrorCode union 同步扩展;已确认随 npm 包发布
  • Android:全部 12 个 reject 点统一 reject(code, message, throwable)(此前混用三种形态,Runner 把 "switchVersion failed" 消息串当 code 传,下载失败 code 是 EUNSPECIFIED
  • iOSPushyErrorCodeKey userInfo 机制承载稳定码;hdiff/解包错误归 PATCH_FAILED;中文文案 "json格式校验报错" 改为 invalid json string补实现 downloadAndInstallApk(reject UNSUPPORTED_PLATFORM,修复新架构下 spec 声明但无实现导致误调即崩)
  • Harmony 有意不动:RNOH rejection 的 code 穿透无真机可验证,JS 层每条路径有 fallback code 兜底
  • native code 经 promise rejection 落在 e.code,JS 层保留不覆盖(有回归测试)

兼容性

  • logger payload 新增 code 字段为纯增量;新事件类型 errorSwitchVersion/errorMarkSuccess 对只处理已知类型的 logger 无影响
  • 行为改进(非破坏):alwaysAlert 下 check/apk 错误现在默认可见(不再要求 throwError:true
  • Android reject code 从 EUNSPECIFIED/消息串变为稳定码——此前的值本就无人可依赖

验证

  • JS:eslint + tsc + 97 单测全绿(新增 6 个错误管道回归测试)
  • Android:全模块 javac 对 RN 0.85.3 release AAR + android-36 编译通过
  • iOS:对 Example Pods 头文件 clang++ -fsyntax-only 通过(仅存量 deprecation warning)
  • Harmony:strict tsc 通过
  • 真机回滚链路行为沿用既有约定(无 iOS/Harmony 真机 CI),功能回归依赖 Android e2e

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a real-time error subscription API (onError) with stable, machine-readable error codes.
    • Introduced typed update errors (UpdateError, UpdateErrorCode) and exposed them from the SDK.
    • Added a new iOS module method for APK install that reports it as unsupported.
  • Bug Fixes

    • Prevented rollback cycles in bundle selection and improved production logging for rollback and rollback sources.
    • Improved download reliability by detecting truncated/incomplete downloads during HTTP fetches.
    • Standardized promise rejections across download/install/patch/switch/rollback flows using consistent error codes.
  • Tests

    • Added coverage for error pipeline and provider rendering behavior.

sunnylqm and others added 2 commits July 7, 2026 19:42
Introduce UpdateError { code, cause, extra } and a single client-side
error exit (emitError: report with code + notify onError listeners).
The provider now surfaces lastError/Alert by subscribing to
client.onError, which fixes check/download errors being invisible to
the Provider UI under the default throwError:false, and removes the
duplicated throwError handling between client and provider.

- report() payload gains a stable machine-readable `code` field
  (purely additive for user loggers)
- previously unreported failures now report: switchVersion /
  switchVersionLater (errorSwitchVersion), markSuccess
  (errorMarkSuccess)
- apk errors keep the original native error (message/stack) and use
  i18n messages instead of bare event-name strings
- export UpdateError / UpdateErrorCode / onError for user code
- remove the dead error_code locale key; add apk error messages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ror codes

Error visibility / robustness (all platforms):
- log the crash-protection rollback (version not marked successful)
  on all three platforms; it was previously fully silent
- Android: persistEditor commit failures now log in release too
- iOS/Harmony: guard the startup rollback loop with a visited set
  (Android already had one) to prevent an infinite spin on corrupted
  state
- iOS: remove the partial version directory when an update fails
  (parity with Android/Harmony) and verify Content-Length on
  downloads, skipping encoded (gzip) responses which NSURLSession
  decompresses transparently

Cross-platform error codes (spec: cpp/patch_core/error_codes.h):
- Android: unify all 12 promise.reject sites to
  reject(code, message, throwable); runners take an errorCode param
  (the operation name was previously passed as the code)
- iOS: carry codes via PushyErrorCodeKey in NSError userInfo; classify
  patch/option/file/download errors; replace the Chinese
  "json格式校验报错" message with "invalid json string"; implement the
  spec'd downloadAndInstallApk (UNSUPPORTED_PLATFORM) which previously
  crashed new-arch callers
- Harmony left unchanged: code propagation through RNOH rejections is
  unverified without a device, and the JS layer stamps fallback codes
  on every path

Verified: javac against RN 0.85.3 AAR, clang -fsyntax-only against
Example Pods, harmony strict tsc, 97 JS unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6ec98ad-073b-4354-9356-de44d7d4aa36

📥 Commits

Reviewing files that changed from the base of the PR and between 414f020 and ba0c759.

📒 Files selected for processing (2)
  • ios/RCTPushy/RCTPushy.mm
  • src/type.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/type.ts
  • ios/RCTPushy/RCTPushy.mm

📝 Walkthrough

Walkthrough

This PR standardizes update error codes across native and JS layers, adds JS error subscription and typed error helpers, and updates rollback handling plus download validation on Android, iOS, and HarmonyOS.

Changes

Native error codes and rollback guards

Layer / File(s) Summary
Android ErrorCodes and runner wiring
android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java, StateSerialRunner.java, UiThreadRunner.java
Adds shared Android error-code constants and threads errorCode through runner promise rejections.
Android module and context updates
android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java, UpdateContext.java
Rejects Android operations with specific error codes and adds error-level logging for persistence and rollback events.
C++ shared error codes
cpp/patch_core/error_codes.h
Defines shared constexpr error-code strings under pushy::error_codes.
iOS coded rejections and rollback guard
ios/RCTPushy/RCTPushy.mm
Uses shared error codes in promise rejections, adds downloadAndInstallApk rejection on iOS, and guards rollback loops with visitedVersions.
iOS download integrity check
ios/RCTPushy/RCTPushyDownloader.mm
Adds a downloaded-file size check for unencoded responses.
HarmonyOS rollback guard and logging
harmony/pushy/src/main/ets/UpdateContext.ts
Adds rollback-cycle guarding and rollback logging in getBundleUrl() and rollBack().

JS error pipeline and provider integration

Layer / File(s) Summary
UpdateError type and exports
src/error.ts, src/index.ts
Adds UpdateErrorCode, UpdateError, toUpdateError, and public re-exports.
Type additions
src/type.ts
Adds errorMarkSuccess and optional error code on event data.
Client onError pipeline
src/client.ts
Adds onError/emitError and updates client error reporting to use stable codes.
Typed throws elsewhere
src/core.ts, src/endpoint.ts
Switches missing-module and no-endpoints failures to UpdateError.
Locale updates
src/locales/en.ts, src/locales/zh.ts
Removes error_code and adds APK-related messages.
Provider integration and cleanup
src/provider.tsx
Subscribes to client.onError, updates error handling, and adds .catch(noop) guards.
Tests
src/__tests__/client.test.ts, src/__tests__/provider.render.test.tsx
Adds error-pipeline coverage and updates provider mocks for onError/emitError.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant PushyClient as Pushy Client
  participant UpdateProvider
  participant NativeModule as Native Module

  App->>UpdateProvider: downloadUpdate()
  UpdateProvider->>PushyClient: switchVersion(hash)
  PushyClient->>NativeModule: switchVersion(hash)
  NativeModule-->>PushyClient: reject(errorCode, message)
  PushyClient->>PushyClient: toUpdateError(err, code)
  PushyClient->>PushyClient: emitError(eventType, err)
  PushyClient-->>UpdateProvider: onError(err, eventType)
  UpdateProvider->>UpdateProvider: setLastError and alertError
  PushyClient-->>App: rethrow if throwError enabled
Loading
sequenceDiagram
  participant NativeUpdate as Native getBundleUrl
  participant StateStore as Persisted State

  NativeUpdate->>StateStore: read stateBeforeLaunch
  NativeUpdate->>StateStore: runStateCore(RESOLVE_LAUNCH)
  StateStore-->>NativeUpdate: launchState with didRollback
  NativeUpdate->>NativeUpdate: log rollback from and to versions
  NativeUpdate->>NativeUpdate: add visited version
  NativeUpdate->>StateStore: check next version
  NativeUpdate->>NativeUpdate: stop on repeated version
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR’s main change: a unified error-handling pipeline with stable cross-platform error codes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/error-handling-pipeline

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/type.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: .eslintrc.js » @react-native/eslint-config#overrides[4]:
Environment key "jest/globals" is unknown

at /node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2079:23
at Array.forEach (<anonymous>)
at ConfigValidator.validateEnvironment (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2073:34)
at ConfigValidator.validateConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2223:18)
at CascadingConfigArrayFactory._finalizeConfigArray (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3985:23)
at CascadingConfigArrayFactory.getConfigArrayForFile (/node_modules/.pnpm/@eslint+eslintrc@2.1.4/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3791:21)
at FileEnumerator._iterateFilesWithFile (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/file-enumerator.js:368:43)
at FileEnumerator._iterateFiles (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/file-enumerator.js:349:25)
at FileEnumerator.iterateFiles (/node_modules/.pnpm/eslint@8.57.1/node_modules/eslint/lib/cli-engine/file-enumerator.js:299:59)
at iterateFiles.next (<anonymous>)

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
ios/RCTPushy/RCTPushy.mm (1)

693-701: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Patch-manifest JSON parse error gets mislabeled as DOWNLOAD_FAILED.

This unclassified NSError (from NSJSONSerialization) has no PushyErrorCodeKey, so it falls through the new downloadUpdate fallback (Line 542-547) and gets tagged kDownloadFailed. But this failure happens after a successful download+unzip, during patch manifest parsing — it's a patch failure, not a download failure. The sibling branches at Line 689 and Line 700 already use kPatchFailed for adjacent manifest issues; this one was missed.

🐛 Proposed fix
     NSError *error = nil;
     id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
     if (error != nil) {
-        callback(error);
+        callback(PushyErrorWithCode(pushy::error_codes::kPatchFailed, error.localizedDescription));
         return;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/RCTPushy/RCTPushy.mm` around lines 693 - 701, The JSON parsing failure in
`RCTPushy.mm` inside the patch-manifest handling path is being reported as a
download error because the `NSError` from `NSJSONSerialization` is passed
through unclassified. Update the `JSONObjectWithData` error branch in the
patch-manifest parsing logic to wrap or translate that error with
`PushyErrorWithCode` using `pushy::error_codes::kPatchFailed`, matching the
neighboring manifest validation branches in the same method, so `downloadUpdate`
will not fall back to `kDownloadFailed` for this post-download parse failure.
🧹 Nitpick comments (1)
src/type.ts (1)

63-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type code with UpdateErrorCode instead of plain string.

error.ts defines UpdateErrorCode specifically to keep error codes stable and safe to aggregate on in a logger, but EventData.code is typed as a loose string, so any direct report()/EventData construction outside emitError won't get compile-time protection against typos in the code value.

♻️ Proposed fix
+import type { UpdateErrorCode } from './error';
+
 export interface EventData {
   /** Stable machine-readable error code (see UpdateErrorCode); present on error events */
-  code?: string;
+  code?: UpdateErrorCode;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/type.ts` around lines 63 - 65, `EventData.code` is too loosely typed, so
direct `report()` or `EventData` construction can use invalid error codes.
Update the `EventData` interface in `type.ts` to use `UpdateErrorCode` for
`code` instead of `string`, and make sure the type is imported from `error.ts`
or re-exported appropriately. Keep `emitError` and any other `EventData`
producers aligned with this stricter type so all error code values are
compile-time checked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 693-701: The JSON parsing failure in `RCTPushy.mm` inside the
patch-manifest handling path is being reported as a download error because the
`NSError` from `NSJSONSerialization` is passed through unclassified. Update the
`JSONObjectWithData` error branch in the patch-manifest parsing logic to wrap or
translate that error with `PushyErrorWithCode` using
`pushy::error_codes::kPatchFailed`, matching the neighboring manifest validation
branches in the same method, so `downloadUpdate` will not fall back to
`kDownloadFailed` for this post-download parse failure.

---

Nitpick comments:
In `@src/type.ts`:
- Around line 63-65: `EventData.code` is too loosely typed, so direct `report()`
or `EventData` construction can use invalid error codes. Update the `EventData`
interface in `type.ts` to use `UpdateErrorCode` for `code` instead of `string`,
and make sure the type is imported from `error.ts` or re-exported appropriately.
Keep `emitError` and any other `EventData` producers aligned with this stricter
type so all error code values are compile-time checked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a5929ae-cef4-4100-ba25-3da0e7b1d73b

📥 Commits

Reviewing files that changed from the base of the PR and between ff55fd3 and 414f020.

📒 Files selected for processing (20)
  • android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java
  • android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java
  • android/src/main/java/cn/reactnative/modules/update/UiThreadRunner.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
  • cpp/patch_core/error_codes.h
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • ios/RCTPushy/RCTPushy.mm
  • ios/RCTPushy/RCTPushyDownloader.mm
  • src/__tests__/client.test.ts
  • src/__tests__/provider.render.test.tsx
  • src/client.ts
  • src/core.ts
  • src/endpoint.ts
  • src/error.ts
  • src/index.ts
  • src/locales/en.ts
  • src/locales/zh.ts
  • src/provider.tsx
  • src/type.ts

- iOS: classify the patch-manifest JSON parse error as PATCH_FAILED;
  it previously fell through unclassified and the downloadUpdate
  fallback mislabeled it DOWNLOAD_FAILED even though the download
  itself had succeeded
- type EventData.code as UpdateErrorCode instead of a loose string for
  compile-time protection against typo'd codes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sunnylqm sunnylqm merged commit 616e206 into master Jul 7, 2026
7 checks passed
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