URL and Networking: NSURLSession, Requests, and the HTTP Round Trip
Learn how Objective-C programs talk to the network using NSURL, NSURLComponents, NSURLRequest, and NSURLSession — from constructing a URL and issuing a request through headers, HTTP methods, status codes, authentication, TLS, timeouts, retries, caching, and upload/download and background transfers.
Almost every non-trivial app eventually needs to talk to a server: fetching a list of products, submitting a form, downloading an image, uploading a file. In Objective-C, that entire world is built on a small, well-layered set of Foundation classes — NSURL to represent an address, NSURLRequest to describe what you want to do with it, and NSURLSession to actually perform the network operation and hand you back a result asynchronously, usually through a block. This lesson walks through that stack end to end, following the same path a real HTTP request takes: from the app, through the session, across TLS, to a server, and back again as data that gets turned into a model and shown on screen. Because networking is asynchronous and fallible by nature, this lesson also spends real time on the things that separate a demo from production code — checking status codes, setting sane timeouts, thinking about retries instead of hammering a failing server, and respecting the platform's caching and security defaults.
NSURL and NSURLComponents: Representing an Address
NSURL is Foundation's representation of a URL — not just a string, but a parsed object that understands the pieces of an address: scheme, host, port, path, query, and fragment. You can build one directly from a well-formed string with [NSURL URLWithString:@"https://api.example.com/v1/users"], and for most simple, fixed URLs that is all you need. NSURL also gives you convenient accessors — .scheme, .host, .path, .query — so you can inspect or log a URL without re-parsing the string yourself.
The trouble with building URLs by hand-concatenating strings — [NSString stringWithFormat:@"%@?q=%@", base, searchTerm] — is that it is fragile and, worse, unsafe: a search term containing spaces, ampersands, or non-ASCII characters will silently produce a malformed or wrong URL unless every piece is percent-escaped correctly. NSURLComponents exists specifically to solve this. It lets you build a URL out of typed pieces — scheme, host, path, and a queryItems array of NSURLQueryItem objects — and it takes care of percent-encoding each piece correctly when it assembles the final .URL.
NSURLComponents also works in the other direction: given an existing URL, you can decompose it back into its pieces, which is exactly what you need when reading query parameters out of an incoming URL (for example, a deep link) rather than constructing one to send. This symmetry — build up from parts, or break down into parts — is the main reason NSURLComponents is the preferred tool over raw string formatting whenever a URL has any dynamic piece at all.
• [NSURL URLWithString:@"..."] — build a URL from a complete, already-valid string. • NSURLComponents — build a URL safely from typed pieces (scheme, host, path, queryItems), with automatic percent-encoding. • NSURLComponents also decomposes an existing URL back into its pieces, including reading queryItems out of it.
NSURLRequest and NSMutableURLRequest: Describing the Operation
An NSURL only says where; NSURLRequest says what. NSURLRequest (and its mutable subclass, NSMutableURLRequest, which you use whenever you need to set anything beyond the URL itself) bundles together the URL, the HTTP method, the headers, the request body, and per-request policies like caching and timeout behavior — everything the networking stack needs to actually perform the operation.
A minimal GET request needs nothing beyond a URL — NSURLRequest defaults its HTTP method to GET. Anything else — sending JSON in a POST body, adding an Authorization header, overriding the default timeout — requires building an NSMutableURLRequest, setting the properties you need (HTTPMethod, HTTPBody, setValue:forHTTPHeaderField:, timeoutInterval), and then either using it directly or, since NSURLRequest and NSMutableURLRequest are toll-free bridged in behavior, passing the mutable request wherever an NSURLRequest is expected.
It is worth internalizing the split of responsibility here: NSURL/NSURLComponents answer 'where', NSURLRequest answers 'how' (method, headers, body, policy), and NSURLSession — covered next — is the engine that actually performs the request and calls you back with the result. Keeping these three responsibilities distinct in your head makes debugging networking code far easier, because a problem is almost always isolated to one of these three layers.
NSURLSession: The Modern Networking Engine
NSURLSession is the class that actually performs network operations. It replaced the older NSURLConnection API and is built around the idea of a session — a configured context that manages a group of related network tasks, with shared settings like cookie storage, cache policy, and credential handling. For quick one-off requests, [NSURLSession sharedSession] gives you a ready-to-use session with system defaults; for anything that needs custom configuration, you create your own session from an NSURLSessionConfiguration.
NSURLSessionConfiguration has three standard flavors worth knowing: defaultSessionConfiguration (uses a persistent, on-disk cache and cookie store, similar to the shared session), ephemeralSessionConfiguration (keeps everything in memory only — no persistent cache, cookies, or credentials, useful for private-browsing-style requests), and backgroundSessionConfiguration: (lets uploads and downloads continue even while the app is suspended or not running, handled by the operating system itself).
Once you have a session, you create a task on it — a data task for fetching data into memory, an upload task for sending a file or data body, or a download task for streaming a response directly to disk — and call resume to actually start it (tasks are created in a suspended state, and forgetting to call resume is a classic first mistake). Each task runs asynchronously, off the calling thread, and reports back through a completion handler block or, for more control, a delegate.
HTTP Methods, Headers, and Query Parameters
The HTTP method tells the server what kind of operation you're asking for, and choosing the right one matters both for correctness and for how intermediaries (caches, proxies, browsers) treat the request. GET retrieves a resource and should never have side effects — it's the method you use for reading data, and it's the one method whose responses are eligible for automatic HTTP caching. POST creates a resource or triggers a non-idempotent action, typically carrying a body. PUT replaces a resource wholesale and is idempotent — sending the same PUT twice should leave the server in the same state as sending it once. DELETE removes a resource. Getting this right isn't pedantry: a GET that secretly mutates server state will be retried, cached, and prefetched in ways that assume it's safe, and that mismatch causes real bugs.
Headers carry metadata about the request or response that doesn't belong in the URL or body: Content-Type tells the server (and, on a response, tells your app) what format the body is in, Authorization carries credentials, Accept tells the server what response formats you can handle. You set request headers with setValue:forHTTPHeaderField: on a mutable request, and you read response headers off the NSHTTPURLResponse you get back in the completion handler.
Query parameters, by contrast, are part of the URL itself — the ?key=value&key2=value2 portion — and are the conventional place for parameters that identify or filter what's being requested on a GET (a search term, a page number, a filter). As covered above, the safe way to add them is through NSURLComponents' queryItems, not hand-built string interpolation, because that guarantees correct percent-encoding of the values.
Status Codes: What Production Code Must Check
A completion handler firing without a low-level transport error (a bad host, no connectivity, a TLS failure) does not mean the request succeeded — it only means the app was able to talk to a server and get some response back. The HTTP status code, found on the NSHTTPURLResponse, is what actually tells you whether the server considered the request successful, and skipping this check is one of the most common real-world networking bugs.
The status code ranges each mean something distinct: 2xx (200 OK, 201 Created, 204 No Content) means success. 3xx means redirection — NSURLSession follows most redirects automatically, but it's still useful to know they exist. 4xx means the client did something wrong — 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found — and retrying the exact same request without changing anything will just fail again. 5xx means the server had a problem — 500 Internal Server Error, 503 Service Unavailable — and these are often the ones worth retrying, since they may be transient.
Production networking code should always cast the response to NSHTTPURLResponse * (it's the concrete subclass NSURLSession hands you for HTTP/HTTPS requests) and check .statusCode before trusting the returned data, rather than assuming that 'no error object' means 'safe to use the body'.
Authentication and TLS
Two authentication patterns come up constantly. Basic authentication sends a username and password, base64-encoded, in an Authorization: Basic ... header — simple, but it must only ever be sent over HTTPS, since base64 is not encryption and is trivially reversible. Bearer token authentication, far more common in modern APIs, sends a previously-obtained token in an Authorization: Bearer <token> header; the token itself is usually issued by a separate login or OAuth flow that is outside the scope of a single request. Conceptually, both patterns boil down to the same thing: attaching credentials to the request via headers so the server can identify who is asking.
TLS (Transport Layer Security) is what turns http:// into https:// — it encrypts the connection between the app and the server so that headers, bodies, and credentials can't be read or tampered with by anything sitting on the network path in between. This is not optional hardening; it is the baseline expectation for any request that carries anything sensitive, which in practice is almost every request a real app makes.
Apple's platforms enforce this expectation through App Transport Security (ATS), which by default refuses plain HTTP connections and requires TLS with reasonably modern settings. Getting an ATS-related error during development is a signal to fix the server's TLS configuration or to use HTTPS in the first place — not to reflexively disable ATS in the app's Info.plist, which quietly reopens the exact insecure behavior ATS exists to prevent.
Timeouts, Retries, and Caching
Every NSURLRequest has a timeoutInterval (in seconds, defaulting to 60) that bounds how long the system will wait for the request to complete before failing it with a timeout error. Setting this thoughtfully matters: too short, and slow-but-successful requests get killed needlessly; too long, and a genuinely stuck request leaves the user staring at a spinner far longer than is reasonable.
When a request does fail, the instinct to immediately retry it can make things worse rather than better — if the failure was because a server is overloaded, an immediate flood of retries from every failed client is exactly the kind of pressure that keeps it overloaded. Real retry logic should be selective (retry on transient failures like timeouts and 5xx responses, not on 4xx client errors that will just fail again) and should back off — waiting progressively longer between attempts — rather than retrying instantly and repeatedly. This lesson only introduces the idea; a full retry-with-backoff implementation is beyond a single module, but knowing the principle is essential to writing networking code that behaves well under real-world failure.
Caching is handled by NSURLCache, which can store HTTP responses (respecting the server's own cache-control headers) so that a subsequent identical request can be satisfied from disk or memory instead of the network. Each request's cachePolicy (on the NSURLRequest, or via the session configuration's requestCachePolicy) controls how aggressively the cache is used — from the default, protocol-respecting behavior, to policies that force a fresh network load, to policies that will return cached data even if it's stale rather than fail.
Upload, Download, and Background Transfers
Besides data tasks (the default, for small request/response bodies held entirely in memory), NSURLSession offers upload tasks — for sending a file or a large body without holding the whole thing as an in-memory NSData, created with uploadTaskWithRequest:fromFile: or ...fromData: — and download tasks, which stream the response directly to a temporary file on disk rather than into memory, appropriate for large downloads like media files.
Where platform support allows, sessions created with a background configuration (backgroundSessionConfigurationWithIdentifier:) let uploads and downloads keep running even after the app is suspended or terminated by the system, with the operating system itself managing the transfer and waking the app (via an app-delegate callback) when it completes or needs attention. This is the mechanism behind things like a large file continuing to upload after a user switches away from the app — genuinely useful, but it comes with real constraints (no arbitrary custom NSURLProtocol behavior, completion handling happens differently) that are worth knowing exist even without implementing them in this lesson.
Putting It Together: A Data Task Example
The example below uses dataTaskWithURL:completionHandler:, the most common entry point for a simple network fetch. Notice that the completion handler is a block — exactly the pattern from the earlier Blocks lesson — capturing whatever context it needs (here, nothing beyond its own parameters) and getting invoked later, asynchronously, once the network operation finishes. Because that block runs on a background queue rather than the main thread, any UI update triggered from inside it must be dispatched back to the main queue — a detail this lesson flags here and the Blocks lesson covers in more depth as a general pattern for asynchronous callbacks.
Reading through the completion handler's three parameters — data, response, error — in the right order is itself a skill: check error first for a transport-level failure, then cast and check the status code on response, and only then treat data as meaningful. Skipping straight to using data because it's non-nil is a common source of bugs, since a non-2xx response can still come with a (non-nil) error body describing what went wrong.
Building and Running a Request
Tasks start suspended — nothing happens until resume is called. The completion handler block runs off the main thread; hop back to the main queue before touching UI. timeoutInterval and headers are set on the request, before the task is created from it.
NSURL *url = [NSURL URLWithString:@"https://api.example.com/v1/users"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"GET";
request.timeoutInterval = 15.0;
[request setValue:@"Bearer abc123" forHTTPHeaderField:@"Authorization"];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// handle the result here
}];
[task resume];Code Examples
This is the workhorse pattern for a simple network fetch: build a URL, hand it to the shared session's dataTaskWithURL:completionHandler:, and supply a block — exactly the completion-handler style from the Blocks lesson — that runs later, asynchronously, with the result. The block checks error first, then the HTTP status code, and only then would go on to use data, and it dispatches any UI work back to the main queue since the handler itself runs on a background queue.
dataTaskWithURL:completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { ... }
Common mistake: Forgetting to call [task resume] — the task is created suspended and silently never runs.
Rather than hand-building the query string with stringWithFormat: (which would need the caller to remember to percent-escape the ampersand and space inside the search term itself), NSURLComponents takes typed NSURLQueryItem values and assembles a correctly-escaped URL. This is the safe, preferred approach whenever any part of a URL comes from dynamic or user-supplied data.
components.queryItems = @[ [NSURLQueryItem queryItemWithName:... value:...], ... ];
This example combines several pieces from this lesson: an explicit HTTP method, a Content-Type header describing the body format, an Authorization header carrying a bearer token, and a JSON-encoded HTTPBody built with NSJSONSerialization (covered in full in the next lesson). The completion handler checks for 201 Created specifically, since that is the conventional success status code for a POST that creates a new resource.
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body options:0 error:&jsonError];
Background sessions use a delegate rather than a completion-handler block, because the transfer may finish while the app itself isn't running to hold a block in memory — the operating system relaunches or wakes the app and calls the delegate method when the download completes. This example only sketches the shape of the pattern; real background-transfer code also implements the app-delegate hook the system calls to let the app know a background event is waiting. Background configurations are identified by a unique string identifier so the system can match a resumed session back to its in-flight transfers across app launches.
NSURL *url = [NSURL URLWithString:@"https://api.example.com/v1/users/42"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"Network error: %@", error.localizedDescription);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
NSLog(@"Unexpected status code: %ld", (long)httpResponse.statusCode);
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// data now holds the successful response body; update the UI here
});
}];
[task resume];NSURLComponents *components = [NSURLComponents componentsWithString:@"https://api.example.com/v1/search"];
components.queryItems = @[
[NSURLQueryItem queryItemWithName:@"q" value:@"objective-c & blocks"],
[NSURLQueryItem queryItemWithName:@"page" value:@"2"]
];
NSURL *url = components.URL;
// https://api.example.com/v1/search?q=objective-c%20%26%20blocks&page=2NSURL *url = [NSURL URLWithString:@"https://api.example.com/v1/users"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.timeoutInterval = 20.0;
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"Bearer abc123" forHTTPHeaderField:@"Authorization"];
NSDictionary *body = @{ @"name": @"Ada Lovelace", @"role": @"engineer" };
NSError *jsonError = nil;
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
options:0
error:&jsonError];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (!error && httpResponse.statusCode == 201) {
NSLog(@"User created successfully");
}
}];
[task resume];NSURLSessionConfiguration *config =
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.example.app.downloads"];
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:nil];
NSURL *fileURL = [NSURL URLWithString:@"https://cdn.example.com/large-video.mp4"];
NSURLSessionDownloadTask *downloadTask = [backgroundSession downloadTaskWithURL:fileURL];
[downloadTask resume];
// Delegate method (partial) called when the download finishes:
// - (void)URLSession:(NSURLSession *)session
// downloadTask:(NSURLSessionDownloadTask *)downloadTask
// didFinishDownloadingToURL:(NSURL *)location
// moves the file from the temporary `location` to a permanent path.The HTTP Request Pipeline
Every network round trip follows this same shape: the app hands a request to NSURLSession, which sends it over TLS to a server, gets back an HTTP response, and the app turns that response's data into a model it can display.
Objective-C App
↓
NSURLSession
↓
HTTP Request
↓
TLS
↓
Server/API
↓
HTTP Response
↓
JSON/Data
↓
Model
↓
UIImportant Rules
Always check the error and the HTTP status code before trusting response data — no error does not mean success.
Never send credentials over plain HTTP; TLS (HTTPS) is the non-negotiable baseline for anything sensitive.
Build URLs with dynamic components using NSURLComponents, not hand-formatted strings, to avoid encoding bugs.
Remember to call resume — tasks are created suspended and do nothing until you start them.
Retry transient failures (timeouts, 5xx) selectively and with backoff; never retry a 4xx client error unchanged.
Why This Matters in Production Apps
Nearly every real Objective-C app on Apple's platforms — from a small utility to a large commercial product — has some version of this exact request pipeline running underneath its UI: a session, a request built with the right method, headers, and body, a response checked for both transport errors and HTTP status codes, and a completion handler that hops back to the main thread before touching anything visual. Getting these fundamentals right early avoids an entire class of bugs — silent failures, stale spinners, retry storms — that are far harder to track down once a networking layer has grown large and is used throughout an app.
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(...) { ... }];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(...) { ... }]; [task resume];
A task created but never resumed simply never runs — no error, no callback, nothing. This is easy to miss because the code compiles and looks complete.
if (data) { /* use data */ } if (error) { /* handle transport failure */ return; } NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) { /* handle server-side failure */ return; } /* now use data */
A non-nil data is not proof of success — a 404 or 500 response commonly still comes with a (non-nil) body describing the error. Skipping the status-code check leads to code that misinterprets error payloads as valid results.
NSString *urlString = [NSString stringWithFormat:@"%@?q=%@", base, userSearchText];
NSURLComponents *components = [NSURLComponents componentsWithString:base]; components.queryItems = @[ [NSURLQueryItem queryItemWithName:@"q" value:userSearchText] ]; NSURL *url = components.URL;
Hand-formatting a query string skips percent-encoding entirely; a search term containing '&', '?', or spaces will silently corrupt the URL or truncate the query. NSURLComponents handles the escaping correctly.
// server returned an error, so retry immediately in a tight loop
// retry only on transient failures (timeout, 5xx), with an increasing delay between attempts
Immediate, unconditional retries can turn a brief server hiccup into a sustained overload, and retrying a 4xx client error wastes effort on a request that will fail identically every time.
What is the primary advantage of building a URL with NSURLComponents and NSURLQueryItem instead of NSString stringWithFormat:? A. It runs faster at runtime B. It automatically percent-encodes each query value correctly, avoiding malformed URLs C. It is required by NSURLSession and will not accept string-built URLs D. It automatically adds HTTPS to the URL
B. It automatically percent-encodes each query value correctly, avoiding malformed URLs — NSURLComponents encodes each piece it's given, so special characters like spaces and ampersands in a query value don't corrupt or truncate the resulting URL the way hand-formatted string interpolation can.
An NSURLSessionDataTask's completion handler fires with error == nil. What can you conclude? A. The request definitely succeeded and data is safe to use as-is B. Only that there was no transport-level failure; the HTTP status code on the response must still be checked C. The server returned a 2xx status code D. The response was served from cache
B. Only that there was no transport-level failure; the HTTP status code on the response must still be checked — A nil error only means the app was able to complete the network exchange; it says nothing about whether the server considered the request successful. That requires separately checking the NSHTTPURLResponse's statusCode.
Which HTTP method should never have side effects and is the only one eligible for standard HTTP response caching? A. POST B. PUT C. GET D. DELETE
C. GET — GET is meant purely for retrieval; intermediaries and caches assume it is safe to repeat, prefetch, and cache, which is why a GET that mutates server state causes real problems.
Why does App Transport Security (ATS) refuse plain HTTP connections by default? A. HTTP is slower than HTTPS B. Apple charges for HTTP traffic C. Without TLS, data traveling over the network — including credentials — can be read or tampered with by anything on the network path D. HTTP requests cannot carry query parameters
C. Without TLS, data traveling over the network — including credentials — can be read or tampered with by anything on the network path — TLS encrypts the connection; without it, headers, bodies, and any credentials sent are exposed to interception, which is exactly what ATS's default HTTPS-only policy is designed to prevent.
Why is retrying a failed request immediately and unconditionally, in a tight loop, a bad practice? A. It uses more battery than a single request B. NSURLSession does not permit more than one request per session C. It can worsen an already-overloaded server, and it retries 4xx client errors that will just fail identically again D. It is not possible to retry with NSURLSession
C. It can worsen an already-overloaded server, and it retries 4xx client errors that will just fail identically again — Immediate, unconditional retries add load exactly when a server is least able to handle it, and retrying a client error (which describes a request that is itself wrong) accomplishes nothing. Selective retries with backoff are the better approach.
What must you remember to call after creating an NSURLSessionDataTask for it to actually run? A. [task start] B. [task resume] C. [task run] D. Nothing — tasks begin automatically upon creation
B. [task resume] — Tasks are created in a suspended state; resume must be called explicitly to begin the network operation, and forgetting it is a common source of silently-nonfunctional networking code.
Write an Objective-C method that takes an NSString search term and a page number, uses NSURLComponents to build a GET request URL against https://api.example.com/v1/search with q and page query parameters, issues the request with NSURLSession's shared session, and in the completion handler checks for both a transport error and a non-2xx status code before logging the raw response data.
NSURLComponents *components = [NSURLComponents componentsWithString:@"https://api.example.com/v1/search"]; components.queryItems = @[ [NSURLQueryItem queryItemWithName:@"q" value:searchTerm], [NSURLQueryItem queryItemWithName:@"page" value:[@(page) stringValue]] ]; NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:components.URL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { NSLog(@"Transport error: %@", error); return; } NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) { NSLog(@"Server error, status %ld", (long)httpResponse.statusCode); return; } NSLog(@"Response data: %@", data); }]; [task resume];
Build the URL from NSURLComponents and an array of NSURLQueryItem, not from stringWithFormat:. Remember: check error, then check httpResponse.statusCode, and only then use data. Don't forget [task resume].