NativeScript / NativeScript/plugins
[background-http][Android] onProgress/onSuccess throw TypeError when the upload service outlives the JS runtime (Task.fromId returns undefined)
まだ誰も着手していません。
- 主要言語
- TypeScript
- スター
- 206
- フォーク
- 123
- 平均マージ
- 2日 1時間
- マージ済み PR(30日)
- 1
説明
Which package(s)
@nativescript/background-http@6.0.2 — Android only.
Environment
@nativescript/core9.0.20, CLI 9.0.6,@nativescript/android9.0.4net.gotev:uploadservice4.9.2 (the plugin's default)- Reported by production users on Android; not reproducible on iOS (see below)
Issue
Task.cache lives only in the JS runtime, but the gotev upload service is a
foreground service that outlives it. If the OS kills the app process while an
upload is in flight, the service keeps going (or is restarted) and eventually
broadcasts progress/completion. The GlobalRequestObserver registered by
init() then dispatches into a fresh runtime whose Task.cache is empty,
Task.fromId() returns undefined, and the handlers dereference it right away.
Two reports from the same upload, in the order they arrived:
Calling js method onProgress failed
TypeError: Cannot read properties of undefined (reading 'setTotalUpload')
at onProgressReceiverProgress(vendor.mjs)
at onProgress(vendor.mjs)
at com.tns.gen.net.gotev.uploadservice.observer.request.RequestObserverDelegate.onProgress(RequestObserverDelegate.java:21)
at net.gotev.uploadservice.observer.request.BaseRequestObserver.onReceive(BaseRequestObserver.kt:29)
Calling js method onSuccess failed
TypeError: Cannot read properties of undefined (reading 'setUpload')
at onProgressReceiverCompleted(vendor.mjs)
at onSuccess(vendor.mjs)
at com.tns.gen.net.gotev.uploadservice.observer.request.RequestObserverDelegate.onSuccess(RequestObserverDelegate.java:32)
at net.gotev.uploadservice.observer.request.BaseRequestObserver.onReceive(BaseRequestObserver.kt:31)
Note the upload itself succeeded — only the JS callback blew up.
Root cause
packages/background-http/index.android.ts on main:
function onProgressReceiverProgress(context: Context, uploadInfo: UploadInfo) {
const uploadId = uploadInfo.getUploadId();
const task = Task.fromId(uploadId); // may be undefined
const totalBytes = uploadInfo.getTotalBytes();
const currentBytes = uploadInfo.getUploadedBytes();
task.setTotalUpload(totalBytes);
Task.fromId (L228) is a plain Task.cache[id] lookup, and all four handlers
use the result unguarded: onProgressReceiverProgress (L42),
onProgressReceiverCompleted (L58), onProgressReceiverCancelled (L90) and
onProgressReceiverError (L97).
iOS is not affected: Task.getTask() (index.ios.ts L277-287) creates a Task
when the native task isn't in the map, instead of returning undefined.
Impact
Without discardUncaughtJsExceptions this is a hard crash — the exception
escapes a native callback. With the flag it is "only" a discarded exception, but
since progress fires repeatedly, a single orphaned upload produces a burst of
them (in our case, one error report per broadcast).
Steps to reproduce
- Start a
multipartUploadlarge enough to take several seconds. - Kill the app process while it runs (
adb shell am force-stop <pkg>, or let
the OS reclaim it) — the upload service survives. - Reopen the app so a fresh runtime calls
init()and registers the observer. - The service reports the resumed/finished upload and the TypeError is thrown.
Suggested fix
Guard the lookup in the four handlers, e.g.:
function onProgressReceiverProgress(context: Context, uploadInfo: UploadInfo) {
const uploadId = uploadInfo.getUploadId();
const task = Task.fromId(uploadId);
+ if (!task) {
+ // Upload started by a previous process: there is no JS task to notify.
+ return;
+ }
Related (bigger, happy to file separately)
Even with the guard, the outcome of an upload started before the restart is
simply lost — the app can never learn whether it succeeded. Two API additions
would make it recoverable: letting the caller supply the upload id (it is
currently generated internally as session.id + '{n}'), and a way to re-attach
listeners to an in-flight or finished upload by id. Today the only workaround is
reconciling against the server after the fact.
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
packages/background-http/index.android.ts から始め、Task.fromId と onProgressReceiverProgress、onProgressReceiverCompleted、onProgressReceiverCancelled、onProgressReceiverError を調べます。長時間の multipartUpload と、必要に応じてプロセスの終了を使って再現します。完了条件は、Task.cache に存在しないアップロードに対するブロードキャストが例外をスローせず安全に戻り、通常の Task コールバックは引き続き実行されることです。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- android, typescript
- 領域
- mobile-dev
- issue の種類
- バグ
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 活発さ
- 静か
- 明瞭さ
- 明確に書かれている
- 初心者へのやさしさ
- 82/100