HttpURLConnection.getResponseCode()冻结执行/不会超时

4
我正在编写一个连接到密码保护的 cPanel 服务器(Apache 2.2.22)页面的 Android 应用程序。当身份验证凭据正确时,我可以轻松连接。然而,当凭据不正确时,我的 Android 应用程序似乎会在 HttpURLConnection.getResponseCode() 方法中冻结。服务器日志显示从我的 Android 设备发送了数百个请求,所有请求都返回预期的 401,但出于某种原因,这在我的应用程序中没有反映出来。
以下是我的代码,在 AsyncTask 中执行:
    @Override
    protected Integer doInBackground(String... bookInfoString) {
        // Stop if cancelled
        if(isCancelled()){
            return null;
        }
        Log.i(getClass().getName(), "SendToDatabase.doInBackground()");

        String apiUrlString = getResources().getString(R.string.url_vages_library);
        try{
            NetworkConnection connection = new NetworkConnection(apiUrlString);
            connection.appendPostData(bookInfoString[0]);
            int responseCode = connection.getResponseCode();
            Log.d(getClass().getName(), "responseCode: " + responseCode);
            return responseCode;
        } catch(IOException e) {
            return null;
        }

    }

这段代码使用了我自己的类NetworkConnection,它只是一个围绕HttpURLConnection的基本包装器类,以避免重复编写代码。以下是代码:

public class NetworkConnection {

    private String url;
    private HttpURLConnection connection;

    public NetworkConnection(String urlString) throws IOException{
        Log.i(getClass().getName(), "Building NetworkConnection for the URL \"" + urlString + "\"");

        url = urlString;
        // Build Connection.
        try{
            URL url = new URL(urlString);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setReadTimeout(1000 /* 1 seconds */);
            connection.setConnectTimeout(1000 /* 1 seconds */);
        } catch (MalformedURLException e) {
            // Impossible: The only two URLs used in the app are taken from string resources.
            e.printStackTrace();
        } catch (ProtocolException e) {
            // Impossible: "GET" is a perfectly valid request method.
            e.printStackTrace();
        }
    }

    public void appendPostData(String postData) {

        try{
            Log.d(getClass().getName(), "appendPostData() called.\n" + postData);

            Log.d(getClass().getName(), "connection.getConnectTimeout(): " + connection.getConnectTimeout());
            Log.d(getClass().getName(), "connection.getReadTimeout(): " + connection.getReadTimeout());

            // Modify connection settings.
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setRequestProperty("Content-Type", "application/json");

            // Get OutputStream and attach POST data.
            OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
            writer.write(postData);
            if(writer != null){
                writer.flush();
                writer.close();
            }

        } catch (SocketTimeoutException e) {
            Log.w(getClass().getName(), "Connection timed out.");
        } catch (ProtocolException e) {
            // Impossible: "POST" is a perfectly valid request method.
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // Impossible: "UTF-8" is a perfectly valid encoding.
            e.printStackTrace();
        } catch (IOException e) {
            // Pretty sure this is impossible but not 100%.
            e.printStackTrace();
        }
    }

    public int getResponseCode() throws IOException{
        Log.i(getClass().getName(), "getResponseCode()");
        int responseCode = connection.getResponseCode();
        Log.i(getClass().getName(), "responseCode: " + responseCode);
        return responseCode;
    }

    public void disconnect(){
        Log.i(getClass().getName(), "disconnect()");
        connection.disconnect();
    }
}

最后,这里是一小部分的logcat日志:
05-03 11:01:16.315: D/vages.library.NetworkConnection(3408): connection.getConnectTimeout(): 1000
05-03 11:01:16.315: D/vages.library.NetworkConnection(3408): connection.getReadTimeout(): 1000
05-03 11:01:16.585: I/vages.library.NetworkConnection(3408): getResponseCode()
05-03 11:04:06.395: I/vages.library.MainActivity$SendToDatabase(3408): SendToDatabase.onPostExecute(null)

您可以看到,该方法似乎只在随机时间后返回null。我等待的最长时间正好是15分钟。在最后两个信息日志之间,dalikvm也有几个内存日志(GC_CONCURRENT),但我已省略。
我还应该说,目前我没有使用https,尽管我不认为这会造成任何问题。无论是完整的答案还是只是告诉我哪些不是问题的评论,我都非常感谢您的反馈,因为我仍然不确定这个问题是服务器端还是客户端的。
非常感谢, William
编辑:我忘了提前提到,我正在使用自己的定制java.net.Authenticator附加我的身份验证凭据。
public class CustomAuthenticator extends Authenticator {

    Context mContext;

    public CustomAuthenticator(Context context){
        super();
        mContext = context;
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
        String username = sharedPreferences.getString(SettingsActivity.KEY_USERNAME_PREFERENCE, null);
        String password = sharedPreferences.getString(SettingsActivity.KEY_PASSWORD_PREFERENCE, null);

        return new PasswordAuthentication(username, password.toCharArray());
    }
}

我在活动的onCreate()方法中设置了:

Authenticator.setDefault(new CustomAuthenticator(mContext));

另外,我使用curl请求密码保护资源,正如预期的那样收到了401错误。现在我假设问题出在客户端。


1
抱歉,也许是我眼瞎了,但我现在看不到您发送身份验证凭据的位置。无论如何,您可以尝试使用curl请求来查看输出类型:curl -v "http://yoururl.com" -u user:password。如果它不起作用,问题可能出在服务器端。 - Esparver
谢谢@Esparver,我已经在curl中尝试过了,它返回了预期的401。所以我猜这是一个客户端问题。我正在使用java.net.Authenticator发送我的身份验证凭据。我将把这段代码附加到我的答案中。 - William Carter
3个回答

3

看起来在使用POST连接时,使用Authenticator存在一个问题。这个问题很久以前就有了,我不知道它是否仍然存在。

我会尝试两件事情:

  • Add a log line in the getPasswordAuthentication of the Authenticator to see if it's effectively called. If nothing is printed, you should check that you add the default Authenticator before it's called. You say you do it in the onCreate(), so it should be fine but it's good to be sure.
  • Avoid using the Authenticator (at least for testing purposes) and send the auth info directly in the HTTP Request. I usually do it this way:

    String auth = user + ":" + pass;
    conn = (HttpURLConnection) url.openConnection();
    conn.setRequestProperty("Authorization", 
                   "Basic " + Base64.encode(auth.getBytes()));
    // Set other parameters and read the result...
    

谢谢您提供如此详细的答案。我在getPasswordAuthentication中放置了一个日志,它被调用了多次。这让我意识到当请求没有授权头部并且详细信息不正确时会返回401,因此我的应用程序会自动发送更多带有错误身份验证凭据的请求。根据REST的规定,客户端应该处理这个问题,所以我在我的CustomAuthenticator中加入了重试计数器。现在它可以工作了,我将发布我的答案,尽管我仍在寻找其他解决方案。 - William Carter
使用 HttpURLConnection.setRequestProperty() 是我之前的身份验证方式。它在运行3.0+设备上完美地工作,但在2.3.4及以下版本中,无论凭据是否正确,我都会收到400错误,而服务器日志根本没有记录请求。我仍在寻找解决这个问题的答案,所以非常感谢您能提供任何其他建议,虽然我可能会为此创建一个新问题。此问题现在已有解决方法,但我仍在寻求知识和真正的解决方案。 - William Carter
当我在使用Authenticator处理PUT请求时,遇到了同样的问题。我将更改我的Authenticator,让用户有机会重新输入凭据或完全取消操作。 - Daniel

2
问题在于当请求头中缺少授权标头并且标头中的凭据不正确时,会发送401未经授权状态。因此,我的应用程序一直无济于事地发送相同的请求。我已经通过将计数器添加到我的CustomAuthenticator中找到了解决问题的方法:
public class CustomAuthenticator extends Authenticator {

    public static int RETRIES = 3;

    int mRetriesLeft;
    Context mContext;

    public CustomAuthenticator(Context context){
        super();
        mRetriesLeft = RETRIES;
        mContext = context;
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {

        Log.i(getClass().getName(), "getPasswordAuthentication() - mCounter: " + mRetriesLeft);

        if(mRetriesLeft > 0){       

            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
            String username = sharedPreferences.getString(SettingsActivity.KEY_USERNAME_PREFERENCE, null);
            String password = sharedPreferences.getString(SettingsActivity.KEY_PASSWORD_PREFERENCE, null);

            mRetriesLeft--;
            return new PasswordAuthentication(username, password.toCharArray());

        } else {
            Log.w(getClass().getName(), "No more retries. Returning null");
            mRetriesLeft = RETRIES;
            return null;
        }
    }

    public void reset(){
        mRetriesLeft = RETRIES;
    }
}

但是我不喜欢这个解决方案,因此没有接受它。每当您发出新请求时(我在AsyncTask.onPreExecute()中执行此操作),您必须记住重置计数器,否则每三个请求将失败。此外,我相信一定有一种本地方法可以做到这一点,尽管我搜遍了文档也找不到。如果有人能指出来,我仍然会非常感激。


0

我不知道我是否正确,但我的解决方案已经连续一整天没有出现任何故障。

尝试这样做

byte[] buf = new byte[4096];
Inputstream is;
do
{
    http conn code etc;
    is=conn.getInputStream();

    if(is.read(buf)==0)             
    {
        flag=1;
    }

    //u can either is.close(); or leave as is

    //code

    int serverResponseCode = connection.getResponseCode();
    String serverResponseMessage = connection.getResponseMessage();     
    conn.disconnect();

} while(flag==1);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接