Skip to content
Summary by AI

Đừng sử dụng HttpClient sai cách! Những sai lầm có thể làm hỏng ứng dụng .NET của bạn

Khi làm việc với HttpClient trong ASP.NET Core, các nhà phát triển thường rơi vào những cái bẫy phổ biến có thể dẫn đến hiệu năng kém, cạn kiệt tài nguyên và các ứng dụng không đáng tin cậy. Đây không chỉ là những vấn đề lý thuyết—đây là...

00:00 / --:--

Read by your browser's built-in voice.

Key Takeaways

Improper HttpClient management in ASP.NET Core can cause critical issues like memory inefficiency, socket exhaustion, and DNS staleness. Using IHttpClientFactory along with modern .NET features ensures reliable, performant, and maintainable HTTP communication.

  • Avoid EnsureSuccessStatusCode() for business logic, as it throws unnecessary exceptions for non-2xx status codes.
  • Utilize ReadFromJsonAsync to deserialize directly from the stream, reducing memory allocations compared to reading strings.
  • Prevent socket exhaustion by replacing manual HttpClient instantiation with IHttpClientFactory for proper handler pooling.
  • Do not use static HttpClient instances, which fail to rotate connections and handle DNS changes in dynamic environments.
  • Avoid modifying client properties per request; instead, configure them in Program.cs and use HttpRequestMessage for specific request customizations.

Khi làm việc với HttpClient trong ASP.NET Core, các nhà phát triển thường rơi vào những cái bẫy phổ biến có thể dẫn đến hiệu năng kém, cạn kiệt tài nguyên và các ứng dụng không đáng tin cậy. Đây không chỉ là những vấn đề lý thuyết—đây là kiểu sự cố khiến các hệ thống sản xuất bị sập vào lúc 2 giờ sáng.

Trong bài viết này, chúng ta sẽ khám phá năm anti-pattern (phản mẫu) quan trọng và các giải pháp được khuyến nghị. Hãy tin tôi, nếu bạn đang sử dụng HttpClient, có lẽ bạn đang mắc phải ít nhất một trong những sai lầm này ngay lúc này.

Anti-Pattern #1: Không xử lý lỗi HTTP đúng cách

❌ Vấn đề

var response = await httpClient.GetAsync("posts/1");
response.EnsureSuccessStatusCode(); // Ném ngoại lệ khi mã trạng thái không thành công

Tại sao nó lại tệ:

  • EnsureSuccessStatusCode() ném ra một HttpRequestException cho bất kỳ mã trạng thái nào không phải 2xx

  • Điều này coi các lỗi logic nghiệp vụ (như 404 Not Found) giống như các lỗi hạ tầng

  • Bạn mất khả năng xử lý các mã trạng thái HTTP khác nhau theo những cách khác nhau

  • Việc xử lý ngoại lệ rất tốn kém và chỉ nên dành cho các trường hợp ngoại lệ

✅ Giải pháp

var response = await httpClient.GetAsync("posts/1");

if (response.IsSuccessStatusCode)
{
    var post = await response.Content.ReadFromJsonAsync<JsonElement>();
    // Xử lý phản hồi thành công
}
else
{
    // Xử lý các mã trạng thái khác nhau một cách phù hợp
    switch (response.StatusCode)
    {
        case System.Net.HttpStatusCode.NotFound:
            // Xử lý 404
            break;
        case System.Net.HttpStatusCode.Unauthorized:
            // Xử lý 401
            break;
        default:
            // Xử lý các lỗi khác
            break;
    }
}

✅ Lợi ích:

  • Kiểm soát tốt hơn việc xử lý lỗi

  • Khả năng phân biệt giữa các mã trạng thái HTTP khác nhau

  • Hiệu quả hơn so với luồng điều khiển dựa trên ngoại lệ

  • Cải thiện trải nghiệm người dùng với các thông báo lỗi cụ thể

Anti-Pattern #2: Đọc nội dung phản hồi hai lần

❌ Vấn đề

var content = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<JsonElement>(content);

Tại sao nó lại tệ:

  • Tạo ra một chuỗi trung gian không cần thiết

  • Lãng phí bộ nhớ bằng cách giữ toàn bộ phản hồi dưới dạng chuỗi

  • Bước giải tuần tự hóa bổ sung là dư thừa

  • Hiệu năng kém, đặc biệt với các payload lớn

⚠️ Tác động đến hiệu năng

Đối với một phản hồi JSON 1MB, về cơ bản bạn đang nhân đôi mức sử dụng bộ nhớ bằng cách tạo cả bản sao chuỗi VÀ đối tượng đã giải tuần tự hóa. Điều này sẽ tăng lên nhanh chóng khi chịu tải!

✅ Giải pháp

var post = await response.Content.ReadFromJsonAsync<JsonElement>();

✅ Lợi ích:

  • Giải tuần tự hóa trực tiếp từ luồng HTTP

  • Giảm phân bổ bộ nhớ

  • Hiệu năng tốt hơn

  • Mã nguồn sạch hơn, dễ đọc hơn

  • Hỗ trợ tích hợp trong .NET 5+

Anti-Pattern #3: Tạo các instance HttpClient mới (Cạn kiệt Socket)

Đây là vấn đề lớn nhất. Sai lầm thực sự làm sập các hệ thống sản xuất.

❌ Vấn đề

using (var httpClient = new HttpClient())
{
    var response = await httpClient.GetAsync("https://api.example.com/posts/1");
    // Xử lý phản hồi
}

Tại sao nó lại tệ:

  • Mỗi instance HttpClient tạo ra các kết nối TCP mới

  • Các kết nối không được giải phóng ngay lập tức khi HttpClient bị hủy (disposed)

  • Dẫn đến cạn kiệt socket khi chịu tải

  • Trạng thái TIME_WAIT có thể giữ các socket bị chiếm dụng trong nhiều phút

  • Có thể gây ra lỗi "Cannot assign requested address" trong môi trường sản xuất

💥 Tác động thực tế:

  • Dưới tải cao, ứng dụng của bạn có thể hết socket khả dụng

  • Mặc dù bạn đang sử dụng các câu lệnh using, các kết nối vẫn ở trạng thái TIME_WAIT

  • Có thể làm sập toàn bộ ứng dụng của bạn

🤔 Giải pháp ban đầu (Tạo ra một vấn đề khác)

Bạn có thể nghĩ: "Tôi sẽ chỉ sử dụng một HttpClient tĩnh để tái sử dụng cùng một instance!"

private static readonly HttpClient _httpClient = new HttpClient();

Cách này giải quyết được vấn đề cạn kiệt socket, nhưng nó lại tạo ra một vấn đề mới: DNS cũ (DNS staleness) (xem Anti-Pattern #4 bên dưới).

✅ Giải pháp thực sự: IHttpClientFactory

// Trong Program.cs
builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

// Trong Controller
public class HomeController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public HomeController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task<IActionResult> GetData()
    {
        var httpClient = _httpClientFactory.CreateClient("MyAPI");
        var response = await httpClient.GetAsync("posts/1");
        // Xử lý phản hồi
    }
}

✅ Lợi ích của IHttpClientFactory:

  • Quản lý vòng đời HttpClient đúng cách

  • Gom nhóm (pool) các instance HttpMessageHandler để tránh cạn kiệt socket

  • Tự động xử lý các thay đổi DNS (xoay vòng các handler mỗi 2 phút theo mặc định)

  • Hỗ trợ các client được đặt tên và được định kiểu

  • Tích hợp với Polly cho các chính sách phục hồi

  • Cho phép cấu hình tập trung

Anti-Pattern #4: Sử dụng HttpClient tĩnh mà không cân nhắc

❌ Vấn đề (Từ "Giải pháp" của Anti-Pattern #3)

private static readonly HttpClient _staticHttpClient = new HttpClient();

Mặc dù cách này giải quyết được tình trạng cạn kiệt socket, nó lại tạo ra các vấn đề mới:

Tại sao nó có vấn đề:

  • HttpClient tĩnh không tuân theo các thay đổi DNS

  • HttpMessageHandler cơ bản lưu trữ kết quả DNS vô thời hạn

  • Có thể gây ra sự cố trong môi trường đám mây với bộ cân bằng tải

  • Nếu IP của dịch vụ thay đổi, ứng dụng của bạn có thể tiếp tục sử dụng IP cũ (đã lỗi thời) trong suốt vòng đời của ứng dụng

  • Không có hỗ trợ tích hợp cho các mô hình phục hồi

  • Trong Kubernetes hoặc môi trường container, điều này có thể định tuyến lưu lượng đến các pod đã bị chấm dứt

🌐 Kịch bản thực tế

Hãy tưởng tượng API của bạn chạy trên Azure với tính năng tự động mở rộng (auto-scaling

  • Việc sửa đổi các thuộc tính mặc định sẽ ảnh hưởng đến instance được gộp (pooled instance)

  • Có thể gây ra tình trạng tranh chấp (race conditions) và hành vi không thể dự đoán trước

  • Các header sẽ tích tụ nếu cùng một instance được tái sử dụng

  • Các vấn đề về an toàn luồng (thread-safety) trong các kịch bản đồng thời

🐛 Lỗi mà bạn sẽ phải mất hàng giờ để truy vết

Request A thiết lập một header. Request B nhận được cùng một client trong pool và nhìn thấy header của Request A. Request B thêm một header khác. Request C nhận được các header trùng lặp. Chào mừng bạn đến với địa ngục gỡ lỗi!

✅ Giải pháp

Cấu hình các client có tên trong Program.cs:

builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

Đối với các header theo từng request, hãy sử dụng HttpRequestMessage:

var httpClient = _httpClientFactory.CreateClient("MyAPI");
var request = new HttpRequestMessage(HttpMethod.Get, "posts/1");
request.Headers.Add("Authorization", $"Bearer {token}");
var response = await httpClient.SendAsync(request);

Ví dụ hoàn chỉnh: Triển khai các phương pháp tốt nhất

Dưới đây là một ví dụ hoàn chỉnh cho thấy tất cả các bản sửa lỗi đã được áp dụng:

Cấu hình Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// Cấu hình client có tên
builder.Services.AddHttpClient("MyAPI", client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

// Cấu hình client kiểu (được khuyến nghị)
builder.Services.AddHttpClient<IPostService, PostService>(client =>
{
    client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

var app = builder.Build();
// ... phần còn lại của cấu hình

Controller với các phương pháp tốt nhất

public class HomeController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;
    
    public HomeController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task<IActionResult> BestPractice()
    {
        try
        {
            var httpClient = _httpClientFactory.CreateClient("MyAPI");
            var response = await httpClient.GetAsync("posts/1");
            
            if (response.IsSuccessStatusCode)
            {
                var post = await response.Content.ReadFromJsonAsync<JsonElement>();
                ViewBag.Method = "Best Practice";
                ViewBag.Title = post.GetProperty("title").GetString();
                ViewBag.Body = post.GetProperty("body").GetString();
            }
            else
            {
                ViewBag.Error = $"HTTP Error: {response.StatusCode}";
            }
        }
        catch (HttpRequestException httpEx)
        {
            // Xử lý các lỗi cấp mạng
            ViewBag.Error = $"Network Error: {httpEx.Message}";
        }
        catch (TaskCanceledException)
        {
            // Xử lý timeout
            ViewBag.Error = "Request timed out";
        }
        catch (Exception ex)
        {
            // Xử lý các lỗi không mong đợi
            ViewBag.Error = $"Error: {ex.Message}";
        }
        
        return View("Index");
    }
}

Tóm tắt: Tài liệu tham khảo nhanh

Anti-Pattern

Vấn đề

Giải pháp

#1: EnsureSuccessStatusCode()

Ném ra ngoại lệ cho tất cả các phản hồi không phải 2xx

Kiểm tra IsSuccessStatusCode và xử lý mã trạng thái một cách rõ ràng

#2: ReadAsStringAsync + Deserialize

Cấp phát bộ nhớ không cần thiết và xử lý kép

Sử dụng trực tiếp ReadFromJsonAsync<T>()

#3: new HttpClient() trong using

Cạn kiệt socket khi chịu tải

Sử dụng IHttpClientFactory

#4: Static HttpClient

DNS cũ, không hỗ trợ khả năng phục hồi

Sử dụng IHttpClientFactory

#5: Thiết lập thuộc tính theo từng request

Tranh chấp, hành vi không thể dự đoán

Cấu hình trong Program.cs, sử dụng HttpRequestMessage để tùy chỉnh theo từng request

Kết luận

Việc sử dụng HttpClient đúng cách là rất quan trọng để xây dựng các ứng dụng ASP.NET Core có khả năng mở rộng và đáng tin cậy. Bằng cách tránh các anti-pattern phổ biến này và tuân theo các phương pháp được khuyến nghị:

  • Ứng dụng của bạn sẽ xử lý được nhiều request đồng thời hơn

  • Bạn sẽ tránh được các vấn đề cạn kiệt socket

  • Việc xử lý lỗi sẽ mạnh mẽ và thân thiện với người dùng hơn

  • Hiệu suất sẽ được cải thiện thông qua việc quản lý bộ nhớ tốt hơn

  • Mã nguồn của bạn sẽ dễ bảo trì và kiểm thử hơn

💡 Ghi nhớ:

Luôn sử dụng IHttpClientFactory trong các ứng dụng ASP.NET Core. Đây là phương pháp được khuyến nghị giúp xử lý tất cả sự phức tạp của việc quản lý vòng đời HttpClient cho bạn.

Bây giờ hãy bắt tay vào thực hiện các request HTTP theo đúng cách! Các máy chủ production của bạn sẽ cảm ơn bạn vì điều đó. 🚀

Rate this article

Rate this article out of 5

No ratings yet
Be the first to rate this article.

Selecting a star will ask you to sign in.
HV
Huy Vũ
Contributor at Selectgo

Shares practical engineering notes on this blog.