Search code examples
androidnanohttpd

Unexpected HTTP 400 status code from NanoHTTPD on Android


Friends!

I'm getting occasional and unexpected HTTP 400 responses from nanohttpd in my Android app. The error is following a specific pattern. I've been looking at this for some time now but I've come to the point where I need a different angle or some other help pointing me in the right direction.

Could you please have a look and share your thoughts or even direct points and suggestions?

  • Why am I getting this HTTP 400 status code?
  • And why only under the given circumstances? (I don't want it at all!)

Some Background

I'm running nanohttpd in my Android project as a temporary isolation layer (due to server side not being mature enough yet). I have isolated the nanohttpd server in an Android Service, which I start from my custom Application object once it's created. This way nanohttpd is not bound to the lifecycle of any particular Activity but can live rather independent of the overall application logic and component life cycles.

The Problem

Now, (almost) everything is working nice and dandy: I can start nanohttpd and perform some initial login requests, my expected mock response is even delivered. When I perform my first "GET" request, though, nanohttpd throws a 400 Bad request status at me, but only the first time. If I back out of the Activity being responsible for the particular "GET" request, and launch it again (from the home screen), it delivers the expected payload with a 200 status, flawlessly.

What Have I Done So Far

I have had a closer look at the nanohttpd source code, trying to track down where and why this 400 status is set. It's not that many places this status code is used. Roughly speaking only here, here and here. Since I'm not dealing with multipart content, I'm left with the first and third "here". But - of course - I can not for my life find neither the root cause of the 400 status, nor which exact block is causing the state for me. When I debug the code, everything works just peachy.

Some Code

This is roughly what my nanohttpd Service (MyNanoHttpdService) looks like:

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    if (ACTION_START.equals(intent.getAction())) {
        String errorMessage = null;

        if (myNanoHttpd == null) {
            String hostUrl = intent.getStringExtra(EXTRA_HOST);
            Uri uri = Utils.notEmpty(hostUrl) ? Uri.parse(hostUrl) : Uri.EMPTY;
            myNanoHttpd = new MyNanoHttpd(this, uri.getHost(), uri.getPort(), null);
        }

        if (!myNanoHttpd.isAlive()) {
            try {
                myNanoHttpd.start();
            } catch (IOException e) {
                StringWriter stringWriter = new StringWriter();
                PrintWriter printWriter = new PrintWriter(stringWriter);
                e.printStackTrace(printWriter);
                errorMessage = stringWriter.toString();
                stopSelf();
            }
        }

        final ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_LISTENER);
        if (resultReceiver != null) {
            int status = myNanoHttpd.isAlive() ? CODE_SUCCESS : CODE_FAILURE;
            Bundle bundle = new Bundle();
            bundle.putString(EXTRA_MESSAGE, errorMessage);
            resultReceiver.send(status, bundle);
        }
    }

    return Service.START_STICKY;
}

And this is how I start the service from my custom Application object, initialize my client side state and fetch some content:

@Override
public void onCreate() {
    super.onCreate();

    // Yes, that is a Java 8 Lambda you see there!
    MyNanoHttpdService
            .start(this, "http://localhost:8080")
            .withStartupListener((status, message) -> {
                if (status == 0) {
                    // POST REQUEST: Works like a charm
                    myNetworkHelper.login();

                    // GET REQUEST: Always fails on first launch
                    myNetworkHelper.getContent();
                } else {
                    Log.e("LOG_TAG", "Couldn't start MyNanoHttpd: " + message);
                }
            });
}

It's safe to assume that the wrapping convenience code (the .withStartupListener(...) - which essentially wraps a ResultReceiver used by the above Service - and the myNetworkHelper object) works as expected. Also, in production, the getContent() call would be made from an Activity or Fragment, but for the sake ease I have moved it to the Application for now.


Solution

  • I may have found the root cause for my issue, and possibly even a workaround for the moment.

    If I'm correct in my investigation, the issue was caused by unconsumed data from a previous (POST) request, contaminating the current (POST) request.

    This line in the NanoHTTPD code base (the header parsing block in the NanoHTTPD.HTTPSession.execute() method, just before calling through to any custom serve(...) method - the third "here" in my question above) was the very line where the HTTP 400 status code was thrown, and just as the code suggests, there was no proper value for the "method" header.

    The value - which I expected to be "POST" in clear text - was contaminated with parts of the JSON content body from the previous request. As soon as I realized this, I tried to consume the entire request body in my custom MyNanoHttpd.serve(IHTTPSession session) method, like so:

    @Override
    public Response serve(IHTTPSesion session) {
        InputStream inputStream = session.getInputStream();
        inputStream.skip(inputStream.available());
    
        // or
        // inputStream.skip(Long.MAX_VALUE);
    
        // or even
        // inputStream.close();
    
        ...
    
    }
    

    This didn't work, though, as I kept getting various exceptions. I ended up gently modifying the NanoHTTPD code, safely closing the input stream in the finally block of the very NanoHTTPD.HTTPSession.execute() method instead.

    I'm, nonetheless, considering reaching out to the NanoHTTPD community to discuss a suitable and sustainable solution.