NSURLRequest: How to handle a redirected post?
Asked Answered
W

2

11

I have a tried and tested use of NSURLRequest (and accompaniments) implementation, that works great for GETs, and POSTs for a given URL.

However, I want to now move the target of the URL without changing the URL used by the app, so I'm intending to use a webhop redirect via my DNS provider.

This works fine for the GET requests, but the POSTs just hang... no connection response is received.

The relevant iOS method for handling a redirect is,

-(NSURLRequest *)connection:(NSURLConnection *)connection
    willSendRequest:(NSURLRequest *)request
    redirectResponse:(NSURLResponse *)redirectResponse

According to Apple's documentation for (handling redirects),

If the delegate doesn't implement connection:willSendRequest:redirectResponse:, all canonical changes and server redirects are allowed.

Well, that's not my experience, because leaving this method out does not work for me. The request just hangs without a response.

Apple also suggests an implementation, of willSendRequest (see above linked Apple documentation), again this doesn't work for me. I see the invocations, but the resulting requests just hang.

My current implementation of willSendRequest is as follows (see below). This follows the redirect, but the handles the request as if it was a GET, rather than a POST.

I believe the problem is that the redirection is losing the fact that the HTTP request is a POST (there may be more problems, such as carrying the request Body forward too?).

I'm not sure what I should be doing here. So any advice on how to correctly handle a POST that receives a redirect would be appreciated. Thanks.

-(NSURLRequest *)connection:(NSURLConnection *)connection
   willSendRequest:(NSURLRequest *)request
  redirectResponse:(NSURLResponse *)redirectResponse
{

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) redirectResponse;

    int statusCode = [httpResponse statusCode];


    NSLog (@"HTTP status %d", statusCode);

    // http statuscodes between 300 & 400 is a redirect ...
    if (httpResponse && statusCode >= 300 && statusCode < 400)
    {
        NSLog(@"willSendRequest (from %@ to %@)", redirectResponse.URL, request.URL);
    }

    if (redirectResponse)
    {
        NSMutableURLRequest *newRequest = [request mutableCopy]; // original request

       [newRequest setURL: [request URL]];

       NSLog (@"redirected");

       return newRequest;
    }
    else
    {
        NSLog (@"original");

       return request;
    }
}

ADDITIONAL INFORMATION 1

The HTTP code received by willSendRequest is 301 - 'Moved Permanently.

Using allHTTPHeaderFields to extract the header fields, I see that he request I originally submit has the header

HTTP header {
  "Content-Length" = 244;
  "Content-Type" = "application/json";
}

...and the copied / redirected request has the header,

Redirect HTTP header {
  Accept = "*/*";
  "Accept-Encoding" = "gzip, deflate";
  "Accept-Language" = "en-us";
  "Content-Type" = "application/json";
}

...which doesn't look like a copy of the original request, or even a superset.

Wellmeaning answered 28/5, 2012 at 10:57 Comment(0)
K
13

Keep your original request, then provide your own willSendRequest:redirectResponse: to customize that request, rather than working with the one Apple provides you.

- (NSURLRequest *)connection: (NSURLConnection *)connection
             willSendRequest: (NSURLRequest *)request
            redirectResponse: (NSURLResponse *)redirectResponse;
{
    if (redirectResponse) {
        // The request you initialized the connection with should be kept as
        // _originalRequest.
        // Instead of trying to merge the pieces of _originalRequest into Cocoa
        // touch's proposed redirect request, we make a mutable copy of the
        // original request, change the URL to match that of the proposed
        // request, and return it as the request to use.
        //
        NSMutableURLRequest *r = [_originalRequest mutableCopy];
        [r setURL: [request URL]];
        return r;
    } else {
        return request;
    }
}

By doing this, you're explicitly ignoring some aspects of the HTTP spec: Redirects should generally be turned into GET requests (depending on the HTTP status code). But in practice, this behaviour will serve you better when POSTing from an iOS application.

See also:

Kolb answered 28/5, 2012 at 15:37 Comment(11)
Hmmm, I think you might be right. I have many overlapping requests, so this will take some sorting (and testing) but I think that's the way to go. Thanks Steven.Wellmeaning
Yep, a quick test indicated that this solution works. Thanks guys!Wellmeaning
You might be able to get the original request from the connection, I can't remember. Worth looking though.Kolb
This method is specified as deprecated after iOS 4.3 though. bit.ly/RqbRn5 Is it ok to implement it then? (I did try out this solution & it works!)Trotta
I believe what's been deprecated is this as part of an informal protocol (on NSObject). It's now available as part of NSURLConnectionDataDelegate instead. So I don't think anything actually changes here.Kolb
Here's an explanation of informal protocols: #2010558Kolb
originalRequest should be renamed to request to use this sample by copy and paste. Apple also provides a good explanation Apple DeveloperItalianism
I'm not sure I'd call that a good explanation, since it outlines every option except this one. But it's good detail, so I've added the iOS version of the link. Thanks.Kolb
Whoops! No, that originalRequest is not a typo. You need to keep a copy of the original request on the delegate object. I'll add an _ to make it more obvious, though.Kolb
no originalRequest should be removed and new request should be made:NSMutableURLRequest *r = [[request mutableCopy] autorelease]; [r setURL: [redirectResponse URL]]; [r setHTTPBody: body]; return r;Beetroot
This post was very helpful but there are a few issues worth mentioning/hilighting: 1) if redirectResponse is nil, then the redirect method was called just to put the request in a canonical form, 2) if the original request was a post, the new one is likely a get (and thus the you'll need to set HTTPMethod to "POST"), 3) the body (HTTPBody) may have become nil and may need to be restored, 4) the headers may have also changed (e.g. Content-Type) and may need to be updated.Overstrung
N
1

The HTTP specification for handling the 3xx class of status codes is very unfriendly towards protocols other than GET and HEAD. It expects some kind of user interaction for at the intermediary step of the redirection, which has lead to a plethora of incompatible client and server implementations, as well as a serious headache for web service developers.

From an iOS NSURL point of view, one of the things you might want to verify is that the original POST body is included in the new, redirect request.

Based off your comments on my original answer, and the edits to your question, it would appear that the URL you are trying to access has been updated permanently (301 status code). In which case you can actually avoid the redirects altogether by using the new URL.

Nath answered 28/5, 2012 at 11:24 Comment(5)
Thanks for your reply, but I'm afraid that's not it. Actually, 'request' holds the redirected URL (as confirmed by my earlier NSLog). I find that counterintuitive too, but that's how it is.Wellmeaning
Thats odd then. You shouldn't have to clone and (re)send the request, if thats the case. Are you stuck with using NSURL classes?Nath
I think I'm stuck with NSURL, is there a better alternative?Wellmeaning
You could try out MKNetworkingKit. In the meantime, can you run your code and add to he question, the exact HTTP status code you're getting?Nath
Thanks for coming back to this and trying to help. I've added some extra info below the original question.Wellmeaning

© 2022 - 2024 — McMap. All rights reserved.