Resolve “Request could not be fulfilled” error upon payment in Hungarian State Railways’ Android application
Introduction
Hungarian State Railways released their Android application back in 2018, which is a convenient alternative to Elvira for browsing the schedule and buying tickets. In 2019, the application was revamped which introduced a mysterious error for some users (including myself) that would always interrupt the payment process without any straight-forward reason. In the meantime, I reverted to using Elvira instead of the application while hoping for the arrival of an official fix.
A few days ago I tried to buy a ticket on Elvira, but the system would not let me log in anymore due to “having” invalid characters in my perfectly valid email address.
Invalid input!
Don't use following charachters in the input fields: [()]'<>%"
After this unfortunate experience, I decided it was time to move on and find out why the application is not working.
The mysterious error
According to my personal experience and the reviews posted on Google Play, the error appears when the user is redirected from the application to the payment processor’s website.
The error is kind of weird, because apparently the payment processor’s page is rendered correctly in the background and the only thing that prevents the user from using it is the appearing dialog which ultimately interrupts the payment process upon clicking the OK button.
Investigation
First, I downloaded the APK file of the application and extracted it using apktool.
PS X:\MAV> apktool d hu.mavszk.vonatinfo_2020-08-31.apk
I: Using Apktool 2.4.1 on hu.mavszk.vonatinfo_2020-08-31.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: X:\apktool\framework\1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
After the extraction of the application files I ran a quick search for the string simplepay and determined which .smali files implement the functionality. Due to copyright reasons I am not going to include the decompiled code here, but if you are interested, you can easily find the error handler function of the WebViewClient in the .smali files.
Next, I replaced the body of the error handler function with some simple logging:
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
Log.i("MAV", view.getUrl());
Log.i("MAV", request.getMethod());
Log.i("MAV", request.getUrl().toString());
Log.i("MAV", String.valueOf(error.getErrorCode()));
Log.i("MAV", error.getDescription().toString());
}
# virtual methods
.method public onReceivedError(Landroid/webkit/WebView;Landroid/webkit/WebResourceRequest;Landroid/webkit/WebResourceError;)V
.locals 2
.param p1, "view" # Landroid/webkit/WebView;
.param p2, "request" # Landroid/webkit/WebResourceRequest;
.param p3, "error" # Landroid/webkit/WebResourceError;
.line 10
const-string v0, "MAV"
invoke-virtual {p1}, Landroid/webkit/WebView;->getUrl()Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
.line 11
const-string v0, "MAV"
invoke-interface {p2}, Landroid/webkit/WebResourceRequest;->getMethod()Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
.line 12
const-string v0, "MAV"
invoke-interface {p2}, Landroid/webkit/WebResourceRequest;->getUrl()Landroid/net/Uri;
move-result-object v1
invoke-virtual {v1}, Landroid/net/Uri;->toString()Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
.line 13
const-string v0, "MAV"
invoke-virtual {p3}, Landroid/webkit/WebResourceError;->getErrorCode()I
move-result v1
invoke-static {v1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
.line 14
const-string v0, "MAV"
invoke-virtual {p3}, Landroid/webkit/WebResourceError;->getDescription()Ljava/lang/CharSequence;
move-result-object v1
invoke-interface {v1}, Ljava/lang/CharSequence;->toString()Ljava/lang/String;
move-result-object v1
invoke-static {v0, v1}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
.line 15
return-void
.end method
To my surprise, I successfully managed to buy a ticket just by replacing the error function with some simple logging, but the root cause of the problem was still unknown.
Root cause
The output of Logcat clearly shows that the problem is related to the loading of the Google Analytics library.
12755 I MAV: https://securepay.simplepay.hu/pay/pay/pspHU/[redacted]
12755 I MAV: GET
12755 I MAV: https://www.google-analytics.com/analytics.js
12755 I MAV: -6
12755 I MAV: net::ERR_CONNECTION_REFUSED
The connection refused error is most likely caused by an adblocker present either on the device or on the network.
But why does the unavailability of the Google Analytics library prevents the placement of a new ticket order? I think a plausible explanation is the deprecation of the old error handler at API level 23.
The documentation of the old (< API level 23) WebViewClient error handler claims that the function is only called for unrecoverable errors:
public void onReceivedError (WebView view, int errorCode, String description, String failingUrl)
Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable). The errorCode parameter corresponds to one of the ERROR_* constants.
However, according to the documentation of the new (≥ API level 23) WebViewClient error handler, it behaves differently as it is called for every single resource on the website:
public void onReceivedError (WebView view, WebResourceRequest request, WebResourceError error)
Report web resource loading error to the host application. These errors usually indicate inability to connect to the server. Note that unlike the deprecated version of the callback, the new version will be called for any resource (iframe, image, etc.), not just for the main page. Thus, it is recommended to perform minimum required work in this callback.
I suspect that the upgrade of the error handler function from the deprecated version to the newer one was made without taking a proper look at the documentation, which introduced this error for numerous users. I would consider this to be a serious problem as it effectively prevents some users from purchasing tickets via the application, which inherently reduces the revenue.
Solution
As a quick workaround, I wrapped the body of the original error handler function in a condition which prevents the cancellation of the payment process when the error equals to net::ERR_CONNECTION_REFUSED.
.method public final onReceivedError(Landroid/webkit/WebView;Landroid/webkit/WebResourceRequest;Landroid/webkit/WebResourceError;)V
.locals 3
invoke-virtual {p3}, Landroid/webkit/WebResourceError;->getErrorCode()I
move-result v2
const/4 v1, -0x6
if-eq v2, v1, :connection_error
[redacted original code]
goto :return
:connection_error
nop
:return
return-void
.end method
Automatization
I created a CI pipeline on GitLab which automatically downloads the latest APK and patches it. The pipeline runs periodically and produces an up-to-date APK which can be installed on devices. Due to copyright reasons, I am not going to publish the patched APK but you should be able to reproduce the results by following the ideas presented in this article.