Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

implement http error handling #119

Merged
merged 3 commits into from
Mar 2, 2018
Merged

Conversation

pb82
Copy link
Contributor

@pb82 pb82 commented Mar 1, 2018

Motivation

Allow better error handling when using the http module and fix a bug in the metrics module. When the return code from a request was 500 the onComplete handler was triggered. The metrics module tried to evaluate the requestError but it was null. The current implementation does set the requestError only when the request itself failed, not when the response returned an invalid status code.

Example:

final HttpResponse resp = httpRequest.execute();
resp.onSuccess(() -> {
  // Do something with the result           
}).onError(() -> {
  // Print an error message, you can safely access resp.getRequestError()
}).onComplete() -> {
  // Close any resources, popups... This will always be called
});

Description

Adds onError and onSuccess handlers to the HttpResponse objects. I've looked at the OkHttp recipes (https://github.com/square/okhttp/wiki/Recipes) and they seem to suggest a similar pattern for error handling.

Tested with:

  • successful request
  • request that returns status 500
  • request to invalid URL

Progress

  • Done Task

@david-martin
Copy link
Member

@pb82 Looks good. Do we use this pattern elsewhere?
Will leave approval to @danielpassos & @secondsun

@pb82
Copy link
Contributor Author

pb82 commented Mar 1, 2018

@david-martin it's the success/error callback pattern that's widely used in async languages. Not sure about Android, but since we already have onComplete i think it makes sense to also have onError.

The problem with onComplete was that you can't be sure that the requestError object was set and before using it you always had to check if it is null (If the server returned 500 the error was not set, only when the request failed completely).

Now when onError is called there will always be a requestError that you can inspect. In the case of a 500 it will look like:

E/AeroGear: Metrics request error java.io.IOException: Request failed with status code 500

There's another use in the auth module where the error handler also could be used: https://github.com/aerogear/aerogear-android-sdk/blob/master/auth/src/main/java/org/aerogear/mobile/auth/credentials/JwksManager.java#L101

(But the old method also still works)

}
MobileCore.getLogger().debug("Metrics sent: " + json.toString());
}).onError(() -> {
MobileCore.getLogger().error("Metrics request error",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@secondsun @danielpassos @wtrocki Should we log anything here? This will happen on devices out in the field, while the logs are only useful when plugged into Android Studio. Are you aware of any conventions about logging in Android apps?

Copy link
Contributor

@wtrocki wtrocki Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see this example aligned with IOS:
https://github.com/aerogear/aerogear-ios-sdk/blob/master/modules/core/metrics/engine/MetricsNetworkPublisher.swift#L20-L26

This can be ok for now. For the cases when network connection is down error will be printed but that's not a big problem IMHO.
I have no strong opinion about it, but would like to keep that aligned with IOS no matter what way we would go.

Copy link
Contributor

@wtrocki wtrocki Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this area may need more work anyway. We could handle 401/403 errors if token is wrong and print different error.

@pb82 pb82 force-pushed the http-error-handling branch 2 times, most recently from c82f277 to 9a1ee34 Compare March 1, 2018 10:31
Copy link
Contributor

@wtrocki wtrocki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to your branch. Reviewed and verified.

@@ -43,4 +43,26 @@ public void testGet() {
response.waitForCompletionAndClose();
}

@Test
public void testErrorHandler() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please @pb82 rename this method accordingly with what the test case is about

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is about testing the error handler?

assertNotNull(response);

response.onComplete(() -> {
// The complete handler must not be called here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call Assert.fail(); instead of comment plus forced bad assertion

@pb82 pb82 force-pushed the http-error-handling branch 2 times, most recently from 9bfe841 to 9e302ba Compare March 1, 2018 11:17
@Override
public HttpResponse onComplete(Runnable completionHandler) {
this.completionHandler = completionHandler;
if (response != null) {
if (response != null && response.isSuccessful()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have any if here. I really don't care if there is a response or it was a success.

Think with me... I have raised a loading popup and wanna dismiss it when the request is complete. Complete should be called always independent of the success or fail.

At the end of the day, request should be always called runCompletionHandler + (runCompletionHandler || runErrorHandler)

It that makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, understand. Then you would see the completion handler more as soemthing like a finally? I'm ok with handling that in the completion handler but then we should probably remove the error handler because you would have to handle a possible error in your completion handler anyway.

@Override
public HttpResponse onError(Runnable errorHandler) {
this.errorHandler = errorHandler;
if (response != null && !response.isSuccessful()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we need this kind of check here. IMO we should check only if errorHandler is null to avoid NPE

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is required because otherwise it could happen that you attach the errorHandler when the network thread has not yet finished and response is still null. In that case we can not yet check if the response was successful and have to wait for the network thread callback to handle that.

@@ -66,8 +87,10 @@ public void waitForCompletionAndClose() {
requestCompleteLatch.await(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
//If a completion Handler was set before this wait then we need to make sure that
// gets called before we free these resources.
if (completionHandler != null) {
if (completionHandler != null && requestError == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I said before if we have a completionHandler it should be always called, not only on success

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, wouldn't that make the error handler pointless? Because then you would have to check for errors in your completion handler.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielpassos I was thinking about the same, but if you read the javadoc of the onComplete method:

/**
* Creates a callback to be called when the response is finished successfully.
*
* The response is delivered immediately if the request has already been finished.
*/

It says that it is only called when the response is finished successfully.

If this is still the case, then I think it shouldn't be called when there is an error.

I think the name of the method is a bit confusing. I agree that if it is called onComplete, then it should be called all the time. Otherwise it's better rename it to onSuccess.

Copy link
Contributor Author

@pb82 pb82 Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wei-lee i'll update this PR soon to include onSuccess, onError and onComplete. Complete will always be called, success only when the request was successful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pb82 👍

* @param runnable a callback method
* @return the instance of the HttpResponse for API chaining
*/
HttpResponse onSuccess(Runnable runnable);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this pretty much make the onComplete redundant ?

I'd assume it's no a "long-running" response, right ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matzew it's like try, catch, finally. onComplete can always be used to clean things up even when there's an error. And you would use onSuccess only if the request succeeded to do things that rely on the data.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matzew Nopz, it's the same way RxJava handler stuffs

http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observer.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielpassos actually, nope - Flowable/Observable are emitting n objects - while Single just emits one. See the subscribe(Consumer<? super T> onSuccess, Consumer<? super Throwable> onError)

that's why I asked if these are long-running requests, where something like onNext(ByteBuffer) would make sense.

--

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this API doesn't support that type of request. When the request gets done things get rolled into memory for better or worse.

runCompletionHandler();

if (!response.isSuccessful()) {
throw new IOException("Request failed with status code " + response.code());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in doubt what we should do here. The request was success called, but the response was not (range [200..300)). This should be a dev responsibility to handle that. IMO we should always call the success expect when the HTTP layer raises a problem.

@secondsun Can you give your opinion here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So first a bit of bikeshedding, IOException is the wrong exception to use here. IOException is more of a "the network quit working" type of failure.

I think that success and error handlers make a lot more sense when we start thinking about ways for modules to "rescue" a http request. If auth can refresh a token then it should be able to register as an error handler and then this distinction makes a lot of sense.

However this kind of changes the semantics a bit. A request is made. A response is returned. success and error handlers are called (which may or may not generate other network activity as appropriate) and when the whole mess has settled the completion handler has finally been called. Completion therefore stops being about a single HTTP request and is instead about a user's action in the application.

Sorry I don't think I made anything more clear LOL

@pb82
Copy link
Contributor Author

pb82 commented Mar 1, 2018

@secondsun I agree with you that IOException is the wrong type for a invalid response even though the OkHttp guys seem to bee happy with it: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/SynchronousGet.java#L33

I've changed that to a custom Exception.

@secondsun @matzew @wei-lee I was discussing this change with @danielpassos and there is one open question about the onSuccess/onError handlers:

Should a response with an error code (e.g. 500) be treated as an error (onError) or treated as a successful request & response (onSuccess) and we only call onError for Network errors?

I'd prefer to call onError for a 500 response but i'm happy to change it if you think that is wrong.

What's your Opinion on this?

@wei-lee
Copy link
Contributor

wei-lee commented Mar 1, 2018

@pb82 I agree with you and I think it should call onError for 5xx responses. In this case, there should be a response object and the developer can use the status code to determine the type of the error.

/**
* Returns the request error if it failed
* @return Exception request error or null
*/
Exception getRequestError();
Exception getHttpError();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just getError?

* Custom exception thrown when a http request returns an unenxpected
* result or result code
*/
public class InvalidResponseException extends Exception {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer extending runtimeexception.

@pb82 pb82 merged commit 1f72c69 into aerogear:master Mar 2, 2018
@pb82 pb82 deleted the http-error-handling branch March 2, 2018 14:00
@tomjackman
Copy link
Contributor

@pb82 Are HTTP 302 Redirects considered errors in a response? I've updated my PR to use the changes here, but it's causing issues with Keycloak, specifically on Keycloak logouts where the result is a HTTP 302 for a successful logout.

E/AeroGear: Error Performing a Logout on the Remote OIDC Server: 
            org.aerogear.mobile.core.http.InvalidResponseException: Unexpected response from server. Status code was: 302
                at org.aerogear.mobile.core.http.OkHttpResponse.lambda$new$0$OkHttpResponse(OkHttpResponse.java:30)

@pb82
Copy link
Contributor Author

pb82 commented Mar 2, 2018

@pb82
Copy link
Contributor Author

pb82 commented Mar 2, 2018

@TommyJ1994 There are three possible ways to resolve this:

  1. we provide our own isSuccesful that includes redirects
  2. we go with @danielpassos approach and consider every response a success and only network errors as failures
  3. we add a onRedirect handler. Probably not a good idea.

@secondsun
Copy link
Contributor

I vote for 2

@tomjackman
Copy link
Contributor

2 for me too for flexibility.

@pb82
Copy link
Contributor Author

pb82 commented Mar 2, 2018

@secondsun @TommyJ1994 Ok, preparing a PR.

@tomjackman
Copy link
Contributor

@pb82 Created an issue for this so you can have a reference for the PR. #123

@danielpassos
Copy link
Collaborator

danielpassos commented Mar 5, 2018

2 is exactly what Okhttp do, they do this because there is a difference between request success and response success. We should handle only the request because there is the lib responsibility. If we could fire the request without any problem we should call success, if not we should call error. Handle the response error is the dev responsibility.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants