TPL Tasks
HttpWebRequest
async programming
.NET
task parallel library

Using TPL Tasks with HttpWebRequest

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The Task Parallel Library (TPL) in .NET provides a powerful abstraction for asynchronous and parallel programming. When HttpWebRequest was the primary HTTP client in .NET, developers needed TPL's Task.Factory.FromAsync to bridge the older Begin/End asynchronous pattern with the modern async/await model. While HttpClient has since replaced HttpWebRequest as the recommended HTTP client, understanding how TPL integrates with HttpWebRequest remains valuable for maintaining legacy codebases and for grasping the evolution of async patterns in .NET.

The Asynchronous Programming Model (APM)

Before async/await existed, .NET used the Asynchronous Programming Model (APM), characterized by BeginOperation and EndOperation method pairs. HttpWebRequest follows this pattern with BeginGetResponse and EndGetResponse. Understanding this pattern explains why Task.Factory.FromAsync exists.

csharp
1// The old APM pattern — callback-based, hard to compose
2HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://api.example.com/data");
3
4request.BeginGetResponse(asyncResult =>
5{
6    HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
7    using var reader = new StreamReader(response.GetResponseStream());
8    string content = reader.ReadToEnd();
9    Console.WriteLine(content);
10}, null);

This callback-based approach works but quickly becomes unwieldy when you need to chain operations, handle errors, or coordinate multiple requests. This is where TPL comes in.

Using Task.Factory.FromAsync

TPL provides Task.Factory.FromAsync to wrap APM method pairs into a Task, making them compatible with async/await. This is the cleanest way to use HttpWebRequest asynchronously.

csharp
1public static async Task<string> FetchDataAsync(string url)
2{
3    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
4    request.Method = "GET";
5    request.Timeout = 30000;
6
7    // Wrap the APM pattern into a Task
8    WebResponse response = await Task.Factory.FromAsync(
9        request.BeginGetResponse,
10        request.EndGetResponse,
11        null);
12
13    using var httpResponse = (HttpWebResponse)response;
14    using var stream = httpResponse.GetResponseStream();
15    using var reader = new StreamReader(stream);
16    return await reader.ReadToEndAsync();
17}

Task.Factory.FromAsync takes the Begin method, the End method, and a state object (typically null), and returns a Task<WebResponse> that completes when the HTTP response arrives. Because it wraps the native async I/O rather than blocking a thread, this approach is efficient even under high concurrency.

Making POST Requests

For POST requests, you write the request body before getting the response. Both steps use FromAsync to stay fully asynchronous:

csharp
1public static async Task<string> PostDataAsync(string url, string jsonBody)
2{
3    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
4    request.Method = "POST";
5    request.ContentType = "application/json";
6
7    using var requestStream = await Task.Factory.FromAsync(
8        request.BeginGetRequestStream, request.EndGetRequestStream, null);
9    byte[] bodyBytes = Encoding.UTF8.GetBytes(jsonBody);
10    await requestStream.WriteAsync(bodyBytes, 0, bodyBytes.Length);
11
12    WebResponse response = await Task.Factory.FromAsync(
13        request.BeginGetResponse, request.EndGetResponse, null);
14    using var httpResponse = (HttpWebResponse)response;
15    using var reader = new StreamReader(httpResponse.GetResponseStream());
16    return await reader.ReadToEndAsync();
17}

Error Handling with WebException

HttpWebRequest throws WebException for HTTP errors, timeouts, and connectivity failures. When wrapped in a Task, these exceptions surface when you await the task, so you handle them with a standard try/catch:

csharp
1public static async Task<string> FetchWithErrorHandlingAsync(string url)
2{
3    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
4
5    try
6    {
7        WebResponse response = await Task.Factory.FromAsync(
8            request.BeginGetResponse,
9            request.EndGetResponse,
10            null);
11
12        using var httpResponse = (HttpWebResponse)response;
13        using var reader = new StreamReader(httpResponse.GetResponseStream());
14        return await reader.ReadToEndAsync();
15    }
16    catch (WebException ex) when (ex.Response is HttpWebResponse errorResponse)
17    {
18        // HTTP error with a response body (4xx, 5xx)
19        using var reader = new StreamReader(errorResponse.GetResponseStream());
20        string errorBody = await reader.ReadToEndAsync();
21        throw new Exception(
22            $"HTTP {(int)errorResponse.StatusCode}: {errorBody}", ex);
23    }
24    catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
25    {
26        throw new TimeoutException(
27            $"Request to {url} timed out after {request.Timeout}ms", ex);
28    }
29    catch (WebException ex)
30    {
31        throw new Exception(
32            $"Request failed: {ex.Status} - {ex.Message}", ex);
33    }
34}

The when clause in C# exception filters lets you handle different failure modes without catching and rethrowing, preserving the original stack trace.

Timeout Handling

HttpWebRequest.Timeout only applies to synchronous calls. For async operations, the timeout behavior depends on the underlying OS socket timeout. To enforce a strict timeout with TPL, use Task.WhenAny with a delay:

csharp
1public static async Task<string> FetchWithTimeoutAsync(string url, int timeoutMs)
2{
3    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
4
5    Task<WebResponse> responseTask = Task.Factory.FromAsync(
6        request.BeginGetResponse,
7        request.EndGetResponse,
8        null);
9
10    Task completed = await Task.WhenAny(responseTask, Task.Delay(timeoutMs));
11
12    if (completed != responseTask)
13    {
14        request.Abort();
15        throw new TimeoutException($"Request to {url} timed out after {timeoutMs}ms");
16    }
17
18    using var httpResponse = (HttpWebResponse)await responseTask;
19    using var reader = new StreamReader(httpResponse.GetResponseStream());
20    return await reader.ReadToEndAsync();
21}

Calling request.Abort() cancels the in-flight request and releases the underlying connection, preventing resource leaks.

Migrating to HttpClient

HttpWebRequest is considered legacy. Microsoft recommends HttpClient for all new development. Here is the equivalent of the above code using HttpClient:

csharp
1private static readonly HttpClient _client = new HttpClient
2{
3    Timeout = TimeSpan.FromSeconds(30)
4};
5
6public static async Task<string> FetchDataModernAsync(string url)
7{
8    HttpResponseMessage response = await _client.GetAsync(url);
9    response.EnsureSuccessStatusCode();
10    return await response.Content.ReadAsStringAsync();
11}

HttpClient is designed to be reused as a single instance (or via IHttpClientFactory in ASP.NET Core). It handles connection pooling, supports CancellationToken natively, and provides a much cleaner API. When working on legacy code, migrating from HttpWebRequest + TPL to HttpClient should be a priority.

Common Pitfalls

  • Creating a new HttpClient per request: Unlike HttpWebRequest, HttpClient is designed to be long-lived. Creating one per request causes socket exhaustion.
  • Ignoring WebException during error handling: Non-2xx HTTP responses throw WebException rather than returning a response object. Always catch and inspect the error response.
  • Not calling request.Abort() on timeout: If you cancel via Task.WhenAny, the underlying connection remains open unless you explicitly abort the request.
  • Not disposing HttpWebResponse: The response and its stream hold network resources. Always wrap them in using statements.
  • Setting Timeout and expecting it to work with async calls: The Timeout property is ignored for async operations. Use Task.WhenAny with Task.Delay or a CancellationTokenSource for async timeouts.

Summary

  • Use Task.Factory.FromAsync to wrap HttpWebRequest's BeginGetResponse/EndGetResponse into awaitable tasks, bridging the APM pattern with async/await.
  • Handle errors by catching WebException with pattern-matched when clauses for HTTP errors, timeouts, and connectivity failures.
  • Enforce async timeouts using Task.WhenAny combined with Task.Delay, and call request.Abort() to clean up timed-out requests.
  • For new projects, migrate to HttpClient, which provides native async/await support, connection pooling, and a simpler API.
  • Always dispose HttpWebResponse objects and reuse HttpClient instances to prevent resource leaks.

Course illustration
Course illustration

All Rights Reserved.