Trong một dự án e-commerce, team BKGlobal gặp một giới hạn tưởng như nhỏ nhưng đủ làm hỏng luồng tìm kiếm: bộ lọc vài trăm SKU khiến request trả về 414. Khi đối chiếu với một reproduction gồm 600 filter, chúng tôi bắt đầu xem xét HTTP QUERY trong .NET 10. Bài viết dưới đây tổng hợp từ RFC 10008 và bài dev.to gốc, kèm hai bài học thực chiến mà team BKGlobal đã đâm vào khi thử ship.
Chúng tôi đã từng gặp đúng bài toán này trong một dự án e-commerce: filter set được lưu lại với vài trăm SKU, endpoint search trả về 414 Request-URI Too Long. Bài viết gốc reproduce lại bằng filter set lên tới 600 SKU, mỗi SKU là một tham số ?sku= lặp lại trong URL, và Kestrel default max request line là 8 KB. Khi URL dài tới 9034 bytes (~600 filters) thì Kestrel trả 414. Đó là lúc chúng tôi bắt đầu nghĩ về QUERY.
Khi nào GET không còn đủ, và tại sao mọi "fix" khác đều là compromise
Trước khi có QUERY, ba lựa chọn quen thuộc đều có vấn đề riêng:
- Body trên GET: spec không định nghĩa, một số proxy silently drop.
- Dùng POST: cache layer skip, gateway không auto-retry, dev đọc docs phải đoán
POST /searchcó phải search hay không. - Cramming filters vào header: nghe hay trong 5 phút rồi thôi.
QUERY giải quyết tất cả bằng một câu: nó là GET có body, idempotent, retry được như GET, cacheable theo đúng nghĩa "request content + metadata" (RFC 10008). Response còn có Content-Location trỏ tới URL để GET kết quả sau.
Cách wire QUERY trong ASP.NET Core 10
.NET 10 ship primitives nhưng không có sugar , không có MapQuery hay [HttpQuery]. Cửa vào là MapMethods:
app.MapMethods("/products/search", [HttpMethods.Query], async (HttpContext ctx) =>
{
var filter = await JsonSerializer.DeserializeAsync<Filter>(ctx.Request.Body);
var result = RunSearch(filter!.Skus, filter.MaxPrice);
ctx.Response.Headers.ContentLocation =
$"/products/search-results/{result.Execution}";
return Results.Ok(result);
});
HttpMethods.Query và HttpMethods.IsQuery là API thật, và IsQuery("query") trả true vì canonicalizer đã handle casing. Phía client:
using var req = new HttpRequestMessage(HttpMethod.Query, "/products/search");
req.Content = new StringContent(json, Encoding.UTF8, "application/json");
using var resp = await http.SendAsync(req);
Kestrel đã parse verb sẵn nên không cần config gì thêm. Test thực tế với 5.000 SKU (thay vì 600 như test ban đầu) cho body 65025 bytes , 200 OK, không còn 414.
Một rule bạn phải tự enforce
Spec nói thẳng: server PHẢI fail request nếu Content-Type thiếu hoặc sai. ASP.NET Core không tự làm cho MapMethods handler, nên bạn phải viết 4 dòng này một lần:
if (string.IsNullOrEmpty(ctx.Request.ContentType))
return Results.Problem("QUERY requires a Content-Type.", statusCode: 400);
if (!ctx.Request.ContentType.StartsWith("application/json",
StringComparison.OrdinalIgnoreCase))
return Results.StatusCode(StatusCodes.Status415UnsupportedMediaType);
Test cho thấy: không Content-Type → 400, text/plain → 415, application/json → 200. Bỏ qua rule này thì client đầu tiên quên gửi header sẽ nhận deserialization exception thay vì một câu trả lời đúng.
Cạm bẫy output caching mà team BKGlobal đã "đâm" vào
Spec nói QUERY responses là cacheable. Mình đã tự tin bật .CacheOutput() lên cả ba endpoint, gửi thử 3 lần mỗi cái:
4) Output caching (execution number only moves on a real hit)
GET x3 -> executions 8, 8, 8 => cached
POST x3 -> executions 9, 10, 11 => NOT cached
QUERY x3 -> executions 12, 13, 14 => NOT cached
GET cache. QUERY thì không. Output caching chỉ coi GET và HEAD là cacheable, cũng dễ hiểu vì QUERY chưa tồn tại khi code đó viết.
Tôi viết một IOutputCachePolicy custom để opt QUERY vào, gọn gàng 20 dòng, chạy thử , cache được. Nhưng gửi hai body khác nhau tới cùng URL thì nhận được cache-sai:
5) Custom policy that caches QUERY (same URL, two different bodies)
body A (2 skus) -> {"matches":2,"execution":15}
body B (4 skus) -> {"matches":2,"execution":15}
Body B hỏi 4 SKU mà nhận câu trả lời của body A, lấy thẳng từ cache. Lý do: cache key lấy từ URL và vary-by rules, mà với QUERY thì URL không phải là query , body mới là query. Policy của tôi đã dạy cache key vào đúng phần duy nhất của request không còn mang thông tin.
Muốn làm đúng phải hash normalized request content vào key, đúng như RFC mô tả và đúng những gì output caching của ASP.NET Core chưa có hook cho. Take-away thực tế: đừng tự viết cache cho QUERY cho tới khi framework có key dựa trên body. Một cache trả lời sai tự tin thì còn tệ hơn không có cache.
Vậy có nên dùng QUERY bây giờ không
Internal API giữa các service mà team kiểm soát được toàn bộ client, gateway và cache layer: có thể pilot QUERY. Việc loại bỏ giới hạn URL là một lợi ích rõ ràng, nhưng vẫn cần kiểm tra observability, retry policy và cache behavior trước khi mở rộng. Thêm vào đó, vì .NET coi QUERY là idempotent, resilience policy sẽ retry nó như retry GET , không cần debate "retry POST /search có safe không".
Public API: cẩn thận hơn. Bài gốc chỉ test trong một container với Kestrel. CDN, WAF, hoặc proxy doanh nghiệp xử lý một verb lạ thế nào thì chưa biết, nên test trước khi ship chứ không phải sau. OpenAPI 3.1 cũng chưa có field query trên path item, đừng hứa với client team rằng endpoint sẽ hiện trong generated document mà không kiểm tra. Giữ route POST sống song song gần như không tốn gì mà có escape hatch.
Source code mẫu
Full runnable sample ở GitHub: https://github.com/ssukhpinder/dev-to-code-samples/tree/main/003-http-query-method
TL;DR cho team
.NET 10cóHttpMethods.Query, primitive thô, dùngMapMethodsđể wire.- Idempotent như GET, có body, không có 414, retry-friendly.
- BẮT BUỘC tự check
Content-Type, spec yêu cầu, framework không tự làm. - Output caching không tự cover QUERY. Đừng tự cache khi chưa có hook key-on-body.
- Internal API: có thể pilot trong phạm vi team kiểm soát, sau khi test gateway, proxy, retry và cache. Public API: pilot trước, giữ POST làm fallback.
Son Do | BKGlobal Tech Team
Architecture is a team sport
#BKGlobal #dotnet #architecture #1percentbetter
Bài liên quan về các quyết định kỹ thuật của team BKGlobal:
- AI coding tools: con số 10x là marketing, thực tế chỉ 25-40% — bài học khi đo năng suất developer.
- Cloudflare pay-per-crawl — các tác động kỹ thuật đối với chiến lược dữ liệu của developer.