macOS NSURLSession

From Lazarus wiki
Revision as of 08:03, 19 September 2020 by Trev (talk | contribs) (New content - first draft)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Overview

The NSURLSession class and related classes provide an API for downloading content via HTTP. The downloading is performed asynchronously, so your application can remain responsive and handle incoming data or errors as they arrive. This class replaces the macOS NSURLConnection class which was deprecated by Apple in macOS 10.11 (El Capitan) in 2015. NSURLSession is available from macOS 10.9 (Mavericks) onwards.

The NSURLSession API is highly asynchronous. If you use the default, system-provided delegate, you must provide a completion handler block that returns data to your application when a transfer finishes successfully or with an error. Alternatively, if you provide your own custom delegate objects, the task objects call those delegates’ methods with data as it is received from the server (or, for file downloads, when the transfer is complete). The NSURLSession API provides status and progress properties, in addition to delivering this information to delegates. It supports cancelling, resuming, and suspending tasks, and it provides the ability to resume suspended, cancelled, or failed downloads where they left off.

The behaviour of the tasks in a session depends on three things:

  1. the type of session (determined by the type of configuration object used to create it),
  2. the type of task, and
  3. whether the application was in the foreground when the task was created.

Types of sessions

The NSURLSession API supports three types of sessions, as determined by the type of configuration object used to create the session:

  1. Default sessions behave similarly to other Foundation methods for downloading URLs. They use a persistent disk-based cache and store credentials in the user’s keychain.
  2. Ephemeral sessions do not store any data to disk; all caches, credential stores, and so on are kept in RAM and tied to the session. Thus, when your application invalidates the session, they are purged automatically.
  3. Background sessions are similar to default sessions, except that a separate process handles all data transfers. Background sessions have some additional limitations:
    1. The session must provide a delegate for uploads and downloads.
    2. Only HTTP and HTTPS protocols are supported.
    3. Only upload and download tasks are supported (no data tasks).
    4. Redirects are always followed.
    5. If the background transfer is initiated while the app is in the background, the configuration object’s discretionary property is treated as being true.

Types of Tasks

An NSURLSession supports three types of tasks:

  • data tasks,
  • download tasks,
  • upload tasks, and
  • webSocket tasks.

Data tasks send and receive data using NSData objects. Data tasks are intended for short, often interactive requests to a server. Data tasks can return data to your application one piece at a time after each piece of data is received, or all at once through a completion handler. As data tasks do not store the data to a file, they are not supported in background sessions.

Download tasks retrieve data in the form of a file, and support background downloads.

Upload tasks send data (usually in the form of a file), and support background uploads.

Web socket tasks exchange messages over TCP and TLS, using the WebSocket protocol defined in RFC 6455.

Protocol support

An NSURLSession natively supports the data, file, ftp, http, and https URL schemes, with transparent support for proxy servers and SOCKS gateways, as configured in the user’s system preferences. It supports HTTP/1.1 and HTTP/2 protocols. HTTP/2, described in RFC 7540, requires a server that supports Application-Layer Protocol Negotiation (ALPN).

Creating a session

The NSURLSession API provides a wide range of configuration options:

  • Private storage support for caches, cookies, credentials, and protocols in a way that is specific to a single session.
  • Authentication, tied to a specific request (task) or group of requests (session).
  • File uploads and downloads by URL, which encourages separation of the data (the file’s contents) from the metadata (the URL and settings).
  • Configuration of the maximum number of connections per host.
  • Per-resource timeouts that are triggered if an entire resource cannot be downloaded in a certain amount of time.
  • Minimum and maximum TLS version support Custom proxy dictionaries.
  • Control over cookie policies.
  • Control over HTTP pipelining behaviour.

When you instantiate a session object, you specify the following:

  • A configuration object that governs the behaviour of that session and the tasks within it
  • Optionally, a delegate object to process incoming data as it is received and handle other events specific to the session and the tasks within it, such as server authentication, determining whether a resource load request should be converted into a download, and so on.
  • If you do not provide a delegate, the NSURLSession object uses a system-provided delegate. In this way, you can readily use NSURLSession in place of existing code that uses the sendAsynchronousRequest:queue:completionHandler: convenience method on NSURLSession.

As these settings are contained in a separate configuration object, you can reuse commonly used settings in other sessions.

Using system-provided delegates

This simple demonstration shows how to use NSURLSession and related classes to download and display a web page using NSURLSession as a drop-in replacement for the NSURLConnection sendAsynchronousRequest:queue:completionHandler: method. Using this method, you need to provide only two pieces of code in your application:

  • Code to create a configuration object and a session based on that object; and
  • A completion handler routine to do something with the data after it has been fully received.

Example code

unit Unit1;

{$mode objfpc}{$H+}
{$modeswitch objectivec1}
{$modeswitch cblocks}

interface

uses
  Forms,      // for the main form
  Dialogs,    // for ShowMessage
  StdCtrls,   // for the button
  SysUtils,   // for sleep
  CocoaAll,   // for NSData and other Cocoa types
  CocoaUtils; // for NSStringString

type
  // setup cBlock for completion handler
  tblock = reference to procedure(data: NSData; response: NSURLResponse; connectionError: NSError); cdecl; cblock;

  // redefine version from packages/cocoaint/src/foundation/NSURLSession.inc
  NSURLSession = objcclass external (NSObject)
  public
    class function sessionWithConfiguration(configuration: NSURLSessionConfiguration): NSURLSession; message 'sessionWithConfiguration:';
  end;

  NSURLSessionAsynchronousConvenience = objccategory external (NSURLSession)
    function dataTaskWithURL_completionHandler(url: NSURL; completionHandler: tBlock): NSURLSessionDataTask; message 'dataTaskWithURL:completionHandler:';
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private

  public

  end;

var
  Form1: TForm1;
  myCache: NSURLcache;
  webData: String = 'default';
  webResponse: String;
  webHTML: String;
  webStatusCode: Integer;
  webErrorCode: Integer = 0;
  webError: String = 'default';
  webErrorReason: String;

implementation

{$R *.lfm}

{ TForm1 }

//{$DEFINE DEBUG}

// Completion handler: Executed after URL has been retrieved or retrieval fails
procedure myCompletionHandler(data: NSData; response: NSURLResponse; connectionError: NSError);
var
  httpResponse: NSHTTPURLResponse;
begin
  {$IFDEF DEBUG}
  NSLog(NSStr('entering completion handler'));
  {$ENDIF}

  // if no error
  if((data.Length > 0) and (connectionError = Nil)) then
    begin
      {$IFDEF DEBUG}
      NSLog(data.description);
      NSLog(response.description);
      NSLog(NSString.alloc.initWithData(data,NSUTF8StringEncoding));
      {$ENDIF}
      webData     :=  NSStringToString(data.description);
      {$IFDEF DEBUG}
      NSLog(NSStr('Web data' + webData));
      {$ENDIF}

      // The NSHTTPURLResponse class is a subclass of NSURLResponse
      // so we can cast an NSURLResponse as an NSHTTPURLResponse
      httpResponse := NSHTTPURLResponse(response);

      // Extract status code from response header
      {$IFDEF DEBUG}
      NSLog(NSStr(IntToStr(httpResponse.statusCode)));
      {$ENDIF}

      webStatusCode :=  httpResponse.statusCode;
      webResponse   :=  NSStringToString(response.description);
      webHTML       :=  NSSTringToString(NSString.alloc.initWithData(data,NSUTF8StringEncoding));
    end
  // o/w return error
  else
    begin
      {$IFDEF DEBUG}
      NSLog(NSStr('Error %@'), connectionError.userInfo);
      {$ENDIF}

      webError := 'Error description: ' + LineEnding +  NSStringToString(connectionError.description);
      webErrorReason := 'Error retrieving: ' + NSStringToString(connectionError.userInfo.valueForKey(NSErrorFailingUrlStringKey))
        + LineEnding + LineEnding + 'Reason: ' + NSStringToString(connectionError.localizedDescription);
    end;

  {$IFDEF DEBUG}
  NSLog(NSStr('leaving completion handler'));
  {$ENDIF}
end;

// Retrieve web page
procedure TForm1.Button1Click(Sender: TObject);
var
  urlSessionConfig: NSURLSessionConfiguration;
  cachePath: NSString;
  urlSession: NSURLSession;
  URL: NSURL;
begin
  // create default url session config
  urlSessionConfig := NSURLSessionConfiguration.defaultSessionConfiguration;

  // configure caching behavior for the default session
  cachePath := NSTemporaryDirectory.stringByAppendingPathComponent(NSStr('/nsurlsessiondemo.cache'));

  {$IFDEF DEBUG}
  NSLog(NSStr('Cache path: %@'), cachePath);
  {$ENDIF}

  myCache := NSURLCache.alloc.initWithMemoryCapacity_diskCapacity_diskPath(16384, 268435456, cachePath);
  urlSessionConfig.setURLCache(myCache);

  // set cache policy
  //urlSessionConfig.setRequestCachePolicy(NSURLRequestReloadIgnoringLocalCacheData);
  urlSessionConfig.setRequestCachePolicy(NSURLRequestUseProtocolCachePolicy);

  // create a session for configuration
  urlSession := NSURLSession.sessionWithConfiguration(urlSessionConfig);

  // create NSURL
  URL := NSURL.URLWithString(NSSTR(PAnsiChar('https://sentinel.sentry.org/xx')));
  if(Url = Nil) then
      ShowMessage('NSURL.URLWithString failed!');

  // setup and execute (resume) data task
  urlSession.dataTaskWithURL_completionHandler(URL, @myCompletionHandler).resume;

  // wait until globals accessible in main thread
  // - the completion handler does not run in the main thread
  while((webData = 'default')) AND (webError = 'default') do
    Sleep(50);

  // display results
  if(webErrorReason <> '') then
    begin
      ShowMessage(webError);
      ShowMessage(webErrorReason);
    end
  else
    begin
      ShowMessage('HTTP status code: ' + IntToStr(webStatusCode) + LineEnding
        + LineEnding + 'Raw data: ' + LineEnding + webData);
      ShowMessage('Response: ' + LineEnding + LineEnding + webResponse);
      ShowMessage('Web page: ' + LineEnding + LineEnding + webHTML);
    end;
end;

Finalization
  // housekeeping
  myCache.release;
end.