-->

Can't set headers on my WKWebView POST request

2019-01-16 11:58发布

问题:

I want to do a POST request to my WKWebView but the headers doesn't get set when I monitor the requests with Charles so the request fails. What is wrong here?

NSString *post = [NSString stringWithFormat: @"email=%@&password=%@", email, password];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSString *contentLength = [NSString stringWithFormat:@"%d", postData.length];

NSURL *url = [NSURL URLWithString:@"http://materik.me/endpoint"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:postData];
[request setValue:contentLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];

[webview loadRequest:request];

And this is what Charles says the request is like:

POST /endpoint HTTP/1.1
Host: materik.me
Content-Type: application/x-www-form-urlencoded
Origin: null
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (iPhone; CPU OS 8_0 like Mac OS X)
Content-Length: 0
Accept-Language: en-us
Accept-Encoding: gzip, deflate

So, as you can see, Content-Length is 0, Accept is not application/json and no request body were sent.

Thanks for any help.

回答1:

I had the same problem with WKWebView, that I decided to use instead of UIWebView to avoid the pickers crash in iOS 8. There are two ways that I can think of:

  1. Use NSURLConnection to make the request and then fill the WKWebView with it's response data. You can find an example here: https://stackoverflow.com/a/10077796/4116680 (You only need connection:didReceiveData: and connectionDidFinishLoading: from the delegate if you don't use a self signed SSL certificate)
  2. Use a JavaScript to make the POST request. Here is an example:

Create file eg. "POSTRequestJS.html":

<html>
    <head>
        <script>
            //POST request example:
            //post('URL', {key: 'value'});
            function post(path, params) {
                var method = "post";
                var form = document.createElement("form");
                form.setAttribute("method", method);
                form.setAttribute("action", path);

                for(var key in params) {
                    if(params.hasOwnProperty(key)) {
                        var hiddenField = document.createElement("input");
                        hiddenField.setAttribute("type", "hidden");
                        hiddenField.setAttribute("name", key);
                        hiddenField.setAttribute("value", params[key]);

                        form.appendChild(hiddenField);
                    }
                }

                document.body.appendChild(form);
                form.submit();
            }
        </script>
    </head>
    <body>
    </body>
</html>

And in your code after where you want to load your request:

NSString *path = [[NSBundle mainBundle] pathForResource:@"POSTRequestJS" ofType:@"html"];
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
WKWebView.navigationDelegate = self;
[WKWebView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];

Add method:

- (void)makePostRequest
{
    NSString *postData = [NSString stringWithFormat: @"email=%@&password=%@", email, password];
    NSString *urlString = @"http://materik.me/endpoint";
    NSString *jscript = [NSString stringWithFormat:@"post('%@', {%@});", urlString, postData];

    DLog(@"Javascript: %@", jscript);

    [WKWebView evaluateJavaScript:jscript completionHandler:nil];

    didMakePostRequest = YES;
}

And last add the WKNavigationDelegate:

- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
    if (!didMakePostRequest) {
        [self makePostRequest];
    }
}


回答2:

As the OP stated, I have also confirmed in Charles that the body is 0 bytes after webView.load(request).

There's a workaround for this WKWebView bug, we will initiate a POST request using URLSession convert the data returned by the server to String and instead of loading the url we will use loadHTMLString which will:

Set the webpage contents and base URL.

and the content is our converted string:

var request = URLRequest(url: URL(string: "http://www.yourWebsite")!)
request.httpMethod = "POST"
let params = "do=something&andAgain=something"
request.httpBody = params.data(using: .utf8)

let task = URLSession.shared.dataTask(with: request) { (data : Data?, response : URLResponse?, error : Error?) in
        if data != nil
        {
            if let returnString = String(data: data!, encoding: .utf8)
            {
                self.webView.loadHTMLString(returnString, baseURL: URL(string: "http://www.yourWebsite.com")!)
            }
        }
}
task.resume()


回答3:

This appears to be a bug.
https://bugs.webkit.org/show_bug.cgi?id=140188

Hopefully it will be addressed soon. In the meantime, reverting to UIWebView or implementing the workaround proposed by Spas Bilyarski in his answer seems to be the best options.



回答4:

I use this delegate method and it works !!!

#pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{

    NSLog(@"%@",navigationAction.request.allHTTPHeaderFields);

    NSString *accessToken = @"Bearer 527d3401f16a8a7955aeae62299dbfbd";
    NSMutableURLRequest *request = [navigationAction.request mutableCopy];

    if(![[request.allHTTPHeaderFields allKeys] containsObject:@"Authorization"]){
        [request setValue:accessToken forHTTPHeaderField:@"Authorization"];

        decisionHandler(WKNavigationActionPolicyCancel);
        [Helper hideProgressHUD];
        [webView loadRequest:request];

    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}


回答5:

I can confirm this problem. A simple workaround for me was an AJAX request, with jQuery:

$.ajax({
    type : 'POST',
    url : $('#checkout-form').attr('action'),
    data : $('#checkout-form').serialize()
}).done(function(response, status) {
    // response if return value 200
}).fail(function(status, error) {
    console.log(error);
});

where my form looks like

<form id="checkout-form" method="POST" action="/shop/checkout">
...
</form>

I hope this helps somebody...



回答6:

workaround: trick by using html5 & javascript.

Add a html5 file with content below to your xcode project. To post data by using javascript & h5 form:

<html>
    <head>
        <script>
            //how to call: post('URL', {"key": "value"});
            function post(path, params) {
                var method = "post";
                var form = document.createElement("form");
                form.setAttribute("method", method);
                form.setAttribute("action", path);
                for(var key in params) {
                    if(params.hasOwnProperty(key)) {
                        var hiddenField = document.createElement("input");
                        hiddenField.setAttribute("type", "hidden");
                        hiddenField.setAttribute("name", key);
                        hiddenField.setAttribute("value", params[key]);
                        form.appendChild(hiddenField);
                    }
                }
                document.body.appendChild(form);
                form.submit();
            }
        </script>
    </head>
    <body>
    </body>
</html>

Load the h5 file to WKWebView:

WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
config.preferences = [[WKPreferences alloc]init];
config.preferences.javaScriptEnabled = YES;
WKWebView* webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
webView.navigationDelegate = self;
[self.view addSubview:webView];
NSString *path = [[NSBundle mainBundle] pathForResource:@"JSPOST" ofType:@"html"];
NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
[webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];

Prepare the parameters to post. ie. a string & an array of dictionary Note: when turn array to json string by using NSJSONSerialization, '\r' may be added automaticly. You must remove all the '\r' in the json string, or the javascript cannot be parsed correctly.

// parameters to post
NSString* name = @"Swift";
NSArray* array = @[@{@"id":@"1", @"age":@"12"}, @{@"id":@"2", @"age":@"22"}];
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:array options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
jsonString = [jsonString stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\'"];
// trim spaces and newline characters
jsonString = [jsonString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
jsonString = [jsonString stringByReplacingOccurrencesOfString:@"\r" withString:@""];
jsonString = [jsonString stringByReplacingOccurrencesOfString:@"\n" withString:@""];
NSString *postData = [NSString stringWithFormat: @"'name':'%@', 'contacts':'%@'", name, jsonString];
// page url to request
NSString *urlStr = @"http:api.example.com/v1/detail";
// javascript to evalute
NSString *jscript = [NSString stringWithFormat:@"post('%@',{%@});", urlStr, postData];
//NSLog(@"Javzascript: %@", jscript);

Put this in the WKWebView's delegate: didFinishNavigation

// call the javascript in step 3
(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
     GCD_MAIN((^{
          [_web evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {
               if (error) {
                   NSLog(@"----------->>>>>>>>>>>>> evaluateJavaScript error : %@", [error localizedDescription]);
               }
          }];
     }));
 }


回答7:

WKWebView.load method doesn't work with post request with post body. You have to use JavaScript to do the trick, check WKWebView.evaluateJavascript.

It maybe a bug, but Apple hasn't addressed it till now.



回答8:

Swift3

Change the baseurl and token to your needs.

extension MyWebView: WKNavigationDelegate {
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
        let request: URLRequest = navigationAction.request
        let urlString: String = request.url?.absoluteString ?? ""
        let baseurl: String = "http://materik.me/"
        let needsAuthorization: Bool = urlString.hasPrefix(baseurl)

        if !needsAuthorization {
            print("url doesn't need authorization. \(urlString)")
            decisionHandler(.allow)
            return
        }

        let headerFields: [String : String] = request.allHTTPHeaderFields ?? [:]
        //print("url: \(urlString) fields: \(headerFields)")

        if headerFields["Authorization"] != nil {
            print("already has authorization header. \(urlString)")
            decisionHandler(.allow)
            return
        }

        print("add authorization header. \(urlString)")
        let token: String = "sw_gu6GpGXRG50PR9oxewI"
        var mutableRequest = request
        mutableRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        decisionHandler(.cancel)
        webView.load(mutableRequest)
    }
}