URLSession Returns HTTP Status Code 200 but Incomplete Response Body
Abstract: Resolve incomplete response bodies from URLSession by implementing URLSessionDataDelegate to buffer streaming data chunks instead of relying on single-shot completion handlers.
2026-10-05
Created: 2026-10-05 by
UserComp.com Editors
When URLSession returns an HTTP status code of 200 but provides an incomplete response body, the issue typically stems from network interruptions, server-side streaming configurations, or incorrect data handling in the completion handler. A 200 OK status indicates that the request was successfully received and processed by the server, regardless of whether the entire payload was transmitted within the specific TCP connection window. Developers often mistake this for a network error, but the underlying cause is usually related to how the client buffers or reads the stream rather than a failure in the HTTP handshake itself.
Understanding why this happens requires looking at the interaction between the network layer and the application logic. The URLSession API is designed to handle both synchronous and asynchronous requests, but when dealing with large payloads or slow connections, the data may arrive in chunks. If your code assumes that all data arrives in a single callback invocation, you will miss subsequent chunks, resulting in truncated content. This behavior is distinct from a timeout or a DNS failure, where the status code would reflect a lower-level network error. Instead, the server has done its job, but the client has not fully consumed the available stream.
Distinguishing Between Network Errors and Partial Data
A common misconception is that any incomplete data transfer should trigger an error object in the completion handler. However, according to Apple’s documentation, the error parameter in the dataTask(_:didReceive:completionHandler:) delegate method is only populated when a networking error occurs, such as a connection reset or timeout. If the server sends a valid HTTP 200 response but closes the connection before sending all bytes, the task completes successfully without an error. The URLResponse object will contain the headers, including the Content-Length, which allows you to compare the expected size against the actual data received.
To diagnose this, you must inspect the URLResponse object. Specifically, check the expectedContentLength property. If this value is greater than zero and significantly larger than the length of the Data object returned, you have confirmed that the body is incomplete. This diagnostic step is crucial because it prevents you from wasting time debugging network permissions or SSL certificates when the issue is purely about data consumption. You should also verify if the server is using chunked transfer encoding, which does not always provide a total content length upfront, making it harder to detect truncation without buffering the entire response.
Handling Streamed Responses Correctly
If you are working with APIs that return large files or continuous streams, relying on the convenience methods like dataTask(with:) that return all data at once can be problematic. These methods buffer the entire response in memory before calling the completion handler. If the network drops mid-stream, the buffered data may be partial, and no error is raised. The robust solution is to implement the URLSessionDataDelegate protocol. By conforming to this protocol, you gain access to the urlSession(_:dataTask:didReceive:) method, which is called every time a new chunk of data arrives.
In this delegate method, you should append incoming data to a mutable Data object. This approach ensures that even if the connection is unstable, you accumulate whatever data has been received so far. Once the task completes, you can process the accumulated data. This pattern is essential for applications that need to display progress or handle very large downloads where memory constraints might otherwise cause issues. It also allows you to implement custom retry logic for specific chunks if necessary, although standard URLSession retries are handled automatically for transient errors.
Implementing the Delegate Pattern
To switch from the convenience method to the delegate pattern, you must initialize your session with a delegate. Create a class that conforms to URLSessionDataDelegate and assign it to the session configuration. Inside the delegate method, use data.append() to store each chunk. Finally, in the urlSession(_:task:didCompleteWithError:) method, check if the error is nil. If it is nil, proceed with processing your complete or partial data. This structure gives you full control over the lifecycle of the data stream and eliminates the ambiguity of missing data in the final callback.
Server-Side Configuration and Headers
Sometimes the issue lies not with the client implementation but with how the server handles the response. Servers configured for high-performance streaming might close the connection prematurely if they detect that the client is not reading fast enough, or if they encounter internal timeouts during generation. Ensure that your server sets appropriate headers such as Transfer-Encoding: chunked if the content length is unknown. Additionally, check if the server is using HTTP/2 multiplexing, which can sometimes lead to head-of-line blocking issues if not managed correctly, though this is less common in standard REST APIs.
It is also worth verifying that the server is not imposing strict limits on response sizes for certain endpoints. Some APIs return a 200 OK with a partial body if the requested resource exceeds a predefined threshold, effectively truncating the response to save bandwidth. Reviewing server logs can confirm if the request was fully processed on the backend. If the server confirms it sent all data, then the problem is definitely on the client side, pointing back to improper buffering or delegate implementation.
Common Pitfalls in Data Processing
Another frequent cause of perceived incompleteness is how the data is decoded after reception. If you attempt to convert a partial Data object into a String or JSON immediately upon receiving the first chunk, you will get malformed output. Always ensure that you wait until the task is fully completed before attempting to parse the content. For JSON responses, use JSONDecoder only after confirming that the entire payload has been collected. If you are parsing line-by-line, ensure that your parser can handle incomplete lines gracefully, waiting for the next chunk to complete the current record.
Furthermore, avoid using global variables to store the response data unless properly synchronized. In asynchronous environments, race conditions can occur if multiple tasks are updating the same data store simultaneously. Using local variables within the scope of the delegate methods or closure handlers is safer. This ensures that each request’s data is isolated and processed independently, preventing cross-contamination of partial results between concurrent network calls.
Frequently Asked Questions
Why doesn't URLSession throw an error for partial downloads?
URLSession treats a successful HTTP 200 response as a completed transaction, even if the body is truncated. Errors are reserved for network-level failures like DNS resolution issues or connection timeouts, not for logical data incompleteness.
How can I detect if the response body is incomplete?
Compare the expectedContentLength from the URLResponse with the actual byte count of the received Data object. If the received data is smaller than the expected length, the body is incomplete.
Is it safe to ignore partial data in production apps?
No, ignoring partial data can lead to corrupted state or failed operations. You should either retry the request, discard the invalid data, or implement a mechanism to resume the download from the last known good byte.
Can chunked transfer encoding cause this issue?
Chunked encoding itself does not cause incompleteness, but it means there is no initial Content-Length. Without knowing the total size upfront, you cannot easily detect truncation until the stream ends, requiring careful buffer management.
What is the best way to handle large file downloads?
Use URLSessionDownloadDelegate instead of URLSessionDataDelegate for large files. This delegates the storage of the data to the system, managing memory efficiently and providing callbacks for download progress and completion.