xref: /aosp_15_r20/external/grpc-grpc-java/documentation/android-binderchannel-status-codes.md (revision e07d83d3ffcef9ecfc9f7f475418ec639ff0e5fe)
1# Android gRPC/BinderChannel Status Codes
2
3## Background
4
5[BinderChannel](https://github.com/grpc/proposal/blob/master/L73-java-binderchannel.md) is a gRPC transport that lets Android apps communicate across processes using familiar gRPC concepts and APIs. A BinderChannel-backed gRPC request can fail for many Android-specific reasons, both at `ServiceConnection` establishment and at `transact()` time. These transport-specific failures must be reported to clients using [gRPC’s standard canonical status code abstraction](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc). This document enumerates the BinderChannel errors one can expect to encounter, specifies a canonical status code mapping for each possibility and discusses how clients should handle them.
6
7
8## Status Code Mapping
9
10Consider the table that follows as an BinderChannel-specific addendum to the “[Codes that may be returned by the gRPC libraries](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc)” document. Mappings in that table that share a status code with one of the binder-specific mappings are repeated here for comparison.
11
12<table>
13  <tr>
14   <td><strong>#</strong>
15   </td>
16   <td><strong>Error Case</strong>
17   </td>
18   <td><strong>Android API Manifestation</strong>
19   </td>
20   <td><strong>Status Code</strong>
21   </td>
22   <td><strong>Expected Client Handling</strong>
23   </td>
24  </tr>
25  <tr>
26   <td>1
27   </td>
28   <td>Server app not installed
29   </td>
30   <td rowspan="5" >bindService() returns false
31   </td>
32   <td rowspan="8" ><p>UNIMPLEMENTED<p>“The operation is not implemented or is not supported / enabled in this service.”
33   </td>
34   <td rowspan="9" >Direct the user to install/reinstall the server app.
35   </td>
36  </tr>
37  <tr>
38   <td>2
39   </td>
40   <td>Old version of the server app doesn’t declare the target android.app.Service in its manifest.
41   </td>
42  </tr>
43  <tr>
44   <td>3
45   </td>
46   <td>Target android.app.Service is disabled
47   </td>
48  </tr>
49  <tr>
50   <td>4
51   </td>
52   <td>The whole server app is disabled
53   </td>
54  </tr>
55  <tr>
56   <td>5
57   </td>
58   <td>Server app predates <a href="https://developer.android.com/guide/topics/permissions/overview">the Android M permissions model</a> and the user must review and approve some newly requested permissions before it can run.
59   </td>
60  </tr>
61  <tr>
62   <td>6
63   </td>
64   <td>Target android.app.Service doesn’t recognize grpc binding Intent (old version of server app?)
65   </td>
66   <td>onNullBinding() ServiceConnection callback
67   </td>
68  </tr>
69  <tr>
70   <td>7
71   </td>
72   <td>Method not found on the io.grpc.Server (old version of server app?)
73   </td>
74   <td rowspan="2" >N/A
75   </td>
76  </tr>
77  <tr>
78   <td>8
79   </td>
80   <td>Request cardinality violation (old version of server app expects unary rather than streaming, say)
81   </td>
82  </tr>
83  <tr>
84   <td>9
85   </td>
86   <td>Old version of the server app exposes target android.app.Service but doesn’t android:export it.
87   </td>
88   <td rowspan="3" >bindService() throws SecurityException
89   </td>
90   <td rowspan="5" ><p>PERMISSION_DENIED<p>
91“The caller does not have permission to execute the specified operation …”
92   </td>
93  </tr>
94  <tr>
95   <td>10
96   </td>
97   <td>Target android.app.Service requires an &lt;android:permission&gt; that client doesn’t hold.
98   </td>
99   <td>Prompt the user to grant the needed Android permission
100   </td>
101  </tr>
102  <tr>
103   <td>11
104   </td>
105   <td>Violations of the security policy for miscellaneous Android features like android:isolatedProcess, android:externalService, android:singleUser, instant apps, BIND_TREAT_LIKE_ACTIVITY, etc,
106   </td>
107   <td rowspan="3" >Give up - This is a programming or packaging error that only the app developer can fix.
108   </td>
109  </tr>
110  <tr>
111   <td>12
112   </td>
113   <td>Calling Android UID not allowed by ServerSecurityPolicy
114   </td>
115   <td rowspan="2" >N/A
116   </td>
117  </tr>
118  <tr>
119   <td>13
120   </td>
121   <td>Server Android UID not allowed by client’s SecurityPolicy
122   </td>
123  </tr>
124  <tr>
125   <td rowspan="3" >14
126   </td>
127   <td rowspan="3" >Server process crashed or killed with request in flight.
128   </td>
129   <td>onDisconnected() ServiceConnection callback
130   </td>
131   <td rowspan="6" ><p>UNAVAILABLE
132<p>
133“The service is currently unavailable. This is most likely a transient condition, which can be corrected by retrying with a backoff ...”
134   </td>
135   <td rowspan="6" >Retry with exponential backoff and deadline (see <a href="https://grpc.github.io/grpc-java/javadoc/io/grpc/ManagedChannelBuilder.html#enableRetry--">ManagedChannelBuilder#enableRetry()</a>
136   </td>
137  </tr>
138  <tr>
139   <td>onBinderDied() IBinder.DeathRecipient callback
140   </td>
141  </tr>
142  <tr>
143   <td>IBinder.transact() throws DeadObjectException
144   </td>
145  </tr>
146  <tr>
147   <td>15
148   </td>
149   <td>Server app is currently being upgraded to a new version
150   </td>
151   <td rowspan="2" >onBindingDied() ServiceConnection callback
152   </td>
153  </tr>
154  <tr>
155   <td>16
156   </td>
157   <td>The whole server app or the target android.app.Service was disabled
158   </td>
159  </tr>
160  <tr>
161   <td>17
162   </td>
163   <td>Binder transaction buffer overflow
164   </td>
165   <td>IBinder.transact() throws TransactionTooLargeException
166   </td>
167  </tr>
168  <tr>
169   <td>18
170   </td>
171   <td>Source Context for bindService() is destroyed with a request in flight
172   </td>
173   <td>onDestroy()
174   </td>
175   <td rowspan="2" ><p>CANCELLED
176<p>
177“The operation was cancelled, typically by the caller.”
178   </td>
179   <td rowspan="2" >Give up for now.
180<p>
181(Re. 18: The caller can try  again later when the user opens the source Activity or restarts the source Service)
182   </td>
183  </tr>
184  <tr>
185   <td>19
186   </td>
187   <td>Client application cancelled the request
188   </td>
189   <td>N/A
190   </td>
191  </tr>
192  <tr>
193   <td rowspan="2" >19
194   </td>
195   <td rowspan="2" >Bug in Android itself or the way the io.grpc.binder transport uses it.
196   </td>
197   <td>IBinder.transact() returns false
198   </td>
199   <td rowspan="4" ><p>INTERNAL
200<p>
201“This means that some invariants expected by the underlying system have been broken. … Reserved for serious errors.”
202   </td>
203   <td rowspan="4" >Give up - This is a programming error that only the app or grpc developers can fix
204   </td>
205  </tr>
206  <tr>
207   <td>bindService() throws IllegalArgumentException
208   </td>
209  </tr>
210  <tr>
211   <td>20
212   </td>
213   <td>Flow-control protocol violation
214   </td>
215   <td rowspan="2" >N/A
216   </td>
217  </tr>
218  <tr>
219   <td>21
220   </td>
221   <td>Can’t parse request/response proto
222   </td>
223  </tr>
224</table>
225
226
227### Ambiguity
228
229We say a status code is ambiguous if it maps to two error cases that reasonable clients want to handle differently. For instance, a client may have good reasons to handle error cases 9 and 10 above differently. But they can’t do so based on status code alone because those error cases map to the same one.
230
231In contrast, for example, even though error case 18 and 19 both map to the status code (`CANCELLED`), they are not ambiguous because we see no reason that clients would want to distinguish them. In both cases, clients will simply give up on the request.
232
233
234#### Ambiguity of PERMISSION_DENIED and Mitigations
235
236The mapping above has only one apparently ambiguous status code: `PERMISSION_DENIED`. However, this isn’t so bad because of the following:
237
238The use of `<android:permission>`s for inter-app IPC access control (error case 10) is uncommon. Instead, we recommend that server apps only allow IPC from a limited set of client apps known in advance and identified by signature.
239
240However, there may be gRPC server apps that want to use custom &lt;android:permission&gt;’s to let the end user decide which arbitrary other apps can make use of its gRPC services. In that case, clients should preempt error case 10 simply by [checking whether they hold the required permissions](https://developer.android.com/training/permissions/requesting) before sending a request.
241
242Server apps can avoid error case 9 by never reusing an android.app.Service as a gRPC host if it has ever been android:exported=false in some previous app version. Instead they should simply create a new android.app.Service for this purpose.
243
244Only error cases 11 - 13 remain, making `PERMISSION_DENIED` unambiguous for the purpose of error handling. Reasonable client apps can handle it in a generic way by displaying an error message and/or proceeding with degraded functionality.
245
246
247#### Non-Ambiguity of UNIMPLEMENTED
248
249The `UNIMPLEMENTED` status code corresponds to quite a few different problems with the server app: It’s either not installed, too old, or disabled in whole or in part. Despite the diversity of underlying error cases, we believe most client apps will and should handle `UNIMPLEMENTED` in the same way: by sending the user to the app store to (re)install the server app. Reinstalling might be overkill for the disabled cases but most end users don't know what it means to enable/disable an app and there’s neither enough space in a UI dialog nor enough reader attention to explain it. Reinstalling is something users likely already understand and very likely to cure problems 1-8.
250
251
252## Detailed Discussion of Binder Failure Modes
253
254### IBinder.transact() returns false
255
256According to the [docs](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)), false “generally means the transaction code was not understood.” This is true for synchronous transactions but all gRPC/BinderChannel transactions are `FLAG_ONEWAY` meaning the calling thread doesn’t wait around for the server to return from `onTransact()`. Examination of the AOSP source code shows several additional undocumented reasons `transact()` could return false but all of these cases should be impossible and aren’t things that reasonable apps want to handle.
257
258### IBinder.transact() Throws an Exception
259
260According to the docs, `transact()` can throw `RemoteException` but the significance of this exception [isn’t documented](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). By inspection of the AOSP source, we see there are several cases:
261
2621. The remote process is no longer alive (`android.os.DeadObjectException`)
2632. The IPC buffer is full (`android.os.TransactionTooLargeException`)
2643. Certain internal errors from the underlying `ioctl()` system call.
265
266Status code mappings:
267<table>
268  <tr>
269   <td><strong>Exception</strong>
270   </td>
271   <td><strong>Status Code</strong>
272   </td>
273   <td><strong>Rationale</strong>
274   </td>
275  </tr>
276  <tr>
277   <td>android.os.DeadObjectException
278   </td>
279   <td>UNAVAILABLE
280   </td>
281   <td>So the caller can retry against a new incarnation of the server process
282   </td>
283  </tr>
284  <tr>
285   <td>android.os.TransactionTooLargeException
286   </td>
287   <td>UNAVAILABLE
288   </td>
289   <td>These are usually transient. A retry is likely to succeed later when demand for the IPC buffer subsides.
290   </td>
291  </tr>
292  <tr>
293   <td>Some other RemoteException
294   </td>
295   <td>INTERNAL
296   </td>
297   <td rowspan="2" >So the caller doesn’t bother retrying
298   </td>
299  </tr>
300  <tr>
301   <td>Some other RuntimeException
302   </td>
303   <td>INTERNAL
304   </td>
305  </tr>
306</table>
307
308
309### bindService() returns false
310
311According to the [docs](https://developer.android.com/reference/android/content/Context#bindService(android.content.Intent,%20android.content.ServiceConnection,%20int)), this bindService() returns false when “the system couldn't find the service or if your client doesn't have permission to bind to it.” However, the part about permission is somewhat misleading.
312
313According to a review of the AOSP source code, there are in fact several cases:
314
3151. The target package is not installed
3162. The target package is installed but does not declare the target Service in its manifest.
3173. The target package requests dangerous permissions but targets sdk &lt;= M and therefore requires a permissions review, but the caller is not running in the foreground and so it would be inappropriate to launch the review UI.
318
319Status code mapping: **UNIMPLEMENTED**
320
321(1) and (2) are interesting new possibilities unique to on-device RPC. (1) is straightforward and the most likely cause of (2) is that the user has an old version of the server app installed that predates its gRPC integration. Many clients will want to handle these cases, likely by directing the user to the app store in order to install/upgrade the server.
322
323Unfortunately `UNIMPLEMENTED` doesn’t capture (3) but none of the other canonical status codes do either and we expect this case to be extremely rare.
324
325
326### bindService() throws SecurityException
327
328According to the [docs](https://developer.android.com/reference/android/content/Context#bindService(android.content.Intent,%20android.content.ServiceConnection,%20int)), SecurityException is thrown “if the calling app does not have permission to bind to the given service”. There are quite a few specific cases:
329
3301. The target Service sets `android:exported = “false”` in its manifest but the caller is in a different app.
3312. The target Service declares a required permission in its manifest but the calling package doesn’t have it.
3323. The target Service is marked as `android:singleton` in the manifest but doesn’t hold the `INTERACT_ACROSS_USERS` permission.
3334. The caller is an `android:isolatedProcess`.
3345. The caller requested certain security-related flags on the binding without the necessary permission (gRPC/BinderChannel doesn’t do this)
3356. … according to the source code, a long tail of unlikely security-related internal errors.
336
337Status code mapping: **PERMISSION_DENIED**.
338
339### bindService() throws IllegalArgumentException
340
341There are a couple cases:
342
3431. The binding Intent is not explicit.
3442. The binding Intent contains file descriptors.
345
346Status Code mapping: **INTERNAL**. These cases should be impossible.
347
348
349### onBindingDied() ServiceConnection callback
350
351According to the [docs](https://developer.android.com/reference/android/content/ServiceConnection#onBindingDied(android.content.ComponentName)): “... This means the interface will never receive another connection. The application will need to unbind and rebind the connection to activate it again. This may happen, for example, if the application hosting the service it is bound to has been updated.”
352
353Status code mapping: **UNAVAILABLE**
354
355`UNAVAILABLE` is the best mapping since a retry is likely to succeed in the near future once the server application finishes updating.
356
357
358### onNullBinding() ServiceConnection callback
359
360According to the [docs](https://developer.android.com/reference/android/content/ServiceConnection#onNullBinding(android.content.ComponentName)): “Called when the service being bound has returned null from its `onBind()` method. This indicates that the attempting service binding represented by this `ServiceConnection` will never become usable.”
361
362Status code mapping: **UNIMPLEMENTED**
363
364`UNIMPLEMENTED` is used here because a retry is likely to fail for the same reason. The most likely root cause for a null binding is an older version of the server app where the `android.app.Service` exists but either doesn’t implement `onBind()` or doesn’t recognize the `grpc.io.action.BIND` Intent action.
365
366
367### onServiceDisconnected() ServiceConnection callback
368
369According to the [docs](https://developer.android.com/reference/android/content/ServiceConnection#onServiceDisconnected(android.content.ComponentName)): “Called when a connection to the Service has been lost. This typically happens when the process hosting the service has crashed or been killed ...”
370
371Status code mapping: **UNAVAILABLE**
372
373`UNAVAILABLE` is used here since a retry is likely to succeed against a newly restarted instance of the server.
374
375
376### Response Parcel Contains an Exception
377
378Android’s Parcel class exposes a mechanism for marshalling certain types of `RuntimeException`s between traditional Binder IPC peers. However we won’t consider this case because gRPC/BinderChannel doesn’t use this mechanism. In fact, all BinderChannel transactions are `FLAG_ONE_WAY` so there is no response Parcel.
379
380
381### Source Context for bindService() is Destroyed
382
383The calling Activity or Service Context might be destroyed with a gRPC request in flight. Apps should cease operations when the Context hosting it goes away and this includes cancelling any outstanding RPCs.
384
385Status code mapping: **CANCELLED**