Using a custom NSURLProtocol with UIWebView and PO

2019-02-12 15:12发布

问题:

In my iOS app, I'm using a UIWebView and a custom protocol (with my own NSURLProtocol implementation). I've been fairly careful about making sure that whenever I load a url, I load something like this into my UIWebView:

myprotocol://myserver/mypath

and in my NSURLProtocol implementation, I take a mutable copy of the NSURLRequest, convert the URL to http: and send that to my server.

Everything works for HTTP GET requests. The problem I encounter is with POST requests. It seems like the UIWebView doesn't properly encode the form data in the HTTPBody if the request uses my custom protocol.

One work-around, since I'm using HTTPS for my server requests, is that I register my protocol handler to intercept http: instead of myprotocol: and I can convert all calls to https: This other question, here, pointed me toward that solution:

But I'm wondering if there's any alternative and/or better way of accomplishing what I want.

回答1:

Instead of trying to use POST requests, one work around is to continue using GET requests for myprotocol:// URLs, but transform them in your NSURLProtocol implementation to an http:// and POST request to your server using the request query string as the body of the POST.

The worry with using GET requests to send large amounts of data is that somewhere along the request chain, the request line might get truncated. This appears to not be a problem, however, with locally-implemented protocols.

I wrote a short Cordova test app to experiment and I found that I was able to send through a little over 1 MiB of data without trouble to the HTTP request echoing service http://http-echo.jgate.de/

Here is my startLoading implementation:

- (void)startLoading {
    NSURL *url = [[self request] URL];
    NSString *query = [url query];
    // Create a copy of `url` without the query string.
    url = [[[NSURL alloc] initWithScheme:@"http" host:@"http-echo.jgate.de" path:[url path]] autorelease];
    NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:url];
    [newRequest setHTTPMethod:@"POST"];
    [newRequest setAllHTTPHeaderFields:[[self request] allHTTPHeaderFields]];
    [newRequest addValue:@"close" forHTTPHeaderField:@"Connection"];
    [newRequest addValue:@"application/x-www-form-urlencoded;charset=UTF-8" forHTTPHeaderField:@"Content-Type"];
    [newRequest setHTTPBody:[query dataUsingEncoding:NSUTF8StringEncoding]];
    urlConnection = [[NSURLConnection alloc] initWithRequest:newRequest delegate:self];
    if (urlConnection) {
        receivedData = [[NSMutableData data] retain];
    }
}

I then implemented the NSURLConnection protocol methods to forward to the appropriate NSURLProtocolClient method, but building up the response data in the case of Transfer-Encoding:chunked (as is the case for responses from http://http-echo.jgate.de/).



回答2:

Unfortunately it looks like that http: and https: scheme requests are handled slightly differently than other (including custom) schemes by Foundation Framework. Obviously HTTPBody and HTTPBodyStream calls on relevant NSURLRequest returns always nil for former ones. This is decided already prior call of [NSURLProtocol canInitWithRequest] therefore custom NSURLProtocol implementation has no way of influencing that (it is too late).

It seems that different NSURLRequest class is used for http: and https: than 'a default one'. Default GnuStep implementation of this class returns always nil from HTTPBody and HTTPBodyStream calls. Therefore particular implementations (e.g. one under PhoneGap, likely part of Foundation Framework) choose NSURLRequest-type of class based on scheme prior consulting that with NSURLProtocol. For custom schemes, you get NSURLRequest that returns nil for both HTTPBody and HTTPBodyStream which effectively disables use of POST method (and other methods with body) in custom URI scheme handler.

Maybe there is a way how to influence decision of which NSURLRequest class is actually used but it is currently unknown to me.

As a workaround, you can still use http: or https: scheme and decide in [NSURLProtocol canInitWithRequest] based on other criteria (e.g. host name).