2

Problem

I need to execute a synchronous HTTP request, without following redirects, preferably without using instance variables, since this is to be incorporated into the j2objc project.

What have I tried

I have tried using NSURLConnection sendSynchronousRequest, which unfortunately cannot easily be told not to follow redirects.

Background

Before telling me that I should not use synchronous requests, please bear in mind that this code is for emulating Java's HttpUrlConnection, which is inherently synchronous in behavior, for the j2objc project. The implementation of IosHttpUrlConnections' native makeSynchronousRequest currently always follows redirects. It should respect the HttpUrlConnection.instanceFollowRedirects field.

Further research conducted

  • When using NSUrlConnection in asynchronous mode, a delegate method is called, which allows for enabling/disabling redirects. However, I need synchronous operation.
  • This answer on NSUrlconnection: How to wait for completion shows how to implement sendSynchronousRequest using an async request. However, I haven't been able to modify it to use a delegate, and thus haven't been able to not follow redirects.

I hope you can help me

Community
  • 1
  • 1
foens
  • 8,642
  • 2
  • 36
  • 48

2 Answers2

1

You can use a NSURLSession with a semaphore, create like this:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];
NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

    if (data)
    {
        // do whatever you want with the data here
    }
    else
    {
        NSLog(@"error = %@", error);
    }

    dispatch_semaphore_signal(semaphore);
}];
[task resume];

// but have the thread wait until the task is done

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

And you have to implement the following method of NSURLSessionTaskDelegate, and call the completionHandler block passing null to stop the redirect.

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest *))completionHandler
Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
gabuh
  • 1,166
  • 8
  • 15
  • Thank your for pointing me in the right direction. I have taken your ideas and implemented it. However, tests show that if I disallow redirects, the code hangs. My question is then: Does this code only work on background threads? It has to work on the main thread/queue as well. Can it be adapted for that? – foens Jan 16 '15 at 08:15
  • Buf... It hangs if you cancel the redirects?, or just if you implement the `- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler method of NSURLSessionTaskDelegate` method? – gabuh Jan 16 '15 at 08:49
  • [This](http://pastebin.com/2MhQsNY7) hangs forever. Ran it on the main thread (on some button click). It works if I change NO to YES in the delegate. – foens Jan 16 '15 at 09:01
  • try sending nil instead of NULL – gabuh Jan 16 '15 at 09:29
  • Actually, it does not wait forever, but the request times out... nil seems to do the same thing. Couldn't it be that somewhere below, the framework needs to use the main thread, but it is blocked waiting for the semaphore? – foens Jan 16 '15 at 09:29
  • 1
    maybe... but I'm not able to understand why works when you are not cancelling the redirect.... I can't try now, I will try later, when in front of the computer – gabuh Jan 16 '15 at 09:34
  • [This answer](http://stackoverflow.com/a/21205992/477854) says that the pattern used in your answer should never be used on the main thread, but he does not say why. – foens Jan 16 '15 at 09:51
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/68947/discussion-between-foens-and-gabuh). – foens Jan 16 '15 at 09:54
1

I guess I'll pick up where they left off, but in Swift since it's so many years later.

class ViewController: UIViewController {

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let semaphore = DispatchSemaphore(value: 0)
        let configuration = URLSessionConfiguration.ephemeral
        configuration.timeoutIntervalForRequest = 10
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)

        // Redirects to google.com
        guard let url = URL(string: "https://bit(dot)ly/19BiSHW") else {
            return
        }

        var data: Data?
        var response: URLResponse?
        var error: Error?
        let task = session.dataTask(with: url) { (innerData, innerResponse, innerError) in
            // For clarity, we'll call this the data task's completion closure

            // Pass the data back to the calling scope
            data = innerData
            response = innerResponse
            error = innerError

            semaphore.signal()
        }

        task.resume()

        if semaphore.wait(timeout: .now() + .seconds(15)) == .timedOut {
            // The task's completion closure wasn't called within the time out, handle appropriately
        } else {
            if let e = error as NSError? {
                if e.domain == NSURLErrorDomain && e.code == NSURLErrorTimedOut {
                    print("The request timed out")
                }

                return
            }

            if let d = data {
                // do whatever you want with the data here, such as print it
                // (the data is the HTTP response body)
                print(String.init(data: d, encoding: .utf8) ?? "Response data could not be turned into a string")

                return
            }

            if let r = response {
                print("No data and no error, but we received a response, we'll inspect the headers")

                if let httpResponse = r as? HTTPURLResponse {
                    print(httpResponse.allHeaderFields)
                }
            }
        }
    }
}

extension ViewController: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Swift.Void) {

        // Inside the delegate method, we will call the delegate's completion handler

        // completionHandler: A block that your handler should call with
        // either the value of the request parameter, a modified URL
        // request object, or NULL to refuse the redirect and return
        // the body of the redirect response.

        // I found that calling the block with nil only triggers the
        // return of the body of the redirect response if the session is ephemeral

        // Calling this will trigger the data task's completion closure
        // which signals the semaphore and allows execution to continue
        completionHandler(nil)
    }
}

What the code is doing:

It is creating an inherently asynchronous task (URLSessionTask), telling it to being execution by calling resume(), then halting the current execution context by waiting on a DispatchSemaphore. This is trick I've seen used, and personally used on many occasions to make something asynchronous behave in a synchronous fashion.

The key point to make is that the code stops execution in the current context. In this example, that context is the main thread (since it is in a UIViewController method), which is generally bad practice. So, if your synchronous code never continues executing (because the semaphore is never signaled) then you UI thread will be stopped forever causing the UI to be frozen.

The final piece is the implementation of the delegate method. The comments suggest that calling completionHandler(nil) should suffice and the documentation supports that. I found that this is only sufficient if you have an ephemeral URLSessionConfiguration. If you have the default configuration, the data task's completion closure doesn't get invoked, so the semaphore never gets signaled, therefore the code to never moves forward. This is what was causing the commenter's/asker's problems of a frozen UI.

allenh
  • 6,582
  • 2
  • 24
  • 40