1 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • RFC 10008 định nghĩa phương thức HTTP QUERY để tài nguyên đích xử lý truy vấn một cách an toàn và idempotent từ phần thân yêu cầu rồi trả về kết quả
  • QUERY kết hợp tính chất safe/idempotent của GET với cách truyền phần thân của POST, giúp giảm URI quá dài, chi phí mã hóa URI, lộ dữ liệu trong log và gánh nặng phải coi mỗi tổ hợp truy vấn là một tài nguyên riêng
  • Máy chủ chỉ có thể xử lý yêu cầu QUERY khi Content-Type và phần thân yêu cầu nhất quán; kiểu không được hỗ trợ, phần thân không khớp và truy vấn không thể xử lý có thể được phân biệt bằng các phản hồi 4xx khác nhau
  • Phản hồi thành công có thể dùng Content-Location để chỉ tài nguyên kết quả truy vấn, và Location để chỉ equivalent resource cho phép thực hiện lại cùng truy vấn đó
  • Phản hồi QUERY có thể được cache, nhưng khóa cache phải bao gồm cả phần thân và metadata; trong môi trường CORS, do không phải là safelisted method nên cần preflight

Mẫu truy vấn HTTP mà QUERY muốn giải quyết

  • RFC 10008 là tài liệu Internet Standards Track định nghĩa phương thức yêu cầu QUERY của HTTP
  • QUERY yêu cầu tài nguyên đích xử lý phần thân yêu cầu và trả về kết quả trong phản hồi
  • Giống POST ở chỗ dùng phần thân, nhưng được định nghĩa là safeidempotent, nên có thể tự động thử lại hoặc khởi động lại
  • Truy vấn GET truyền thống thường đưa đầu vào vào URI
    • GET /feed?q=foo&limit=10&sort=-published HTTP/1.1
  • Khi đưa dữ liệu truy vấn vào URI, giới hạn sẽ tăng lên theo kích thước dữ liệu
    • Vì đi qua nhiều hệ thống độc lập nên khó biết trước giới hạn kích thước URI thực tế
    • HTTP khuyến nghị bên gửi và bên nhận hỗ trợ tối thiểu 8000 octets, nhưng không đảm bảo cho mọi hệ thống trên đường đi
    • Một số dữ liệu tốn kém để mã hóa thành URI hợp lệ
    • URI của yêu cầu dễ bị ghi log hoặc được lưu bookmark hơn phần thân yêu cầu
    • Nếu mã hóa truy vấn trực tiếp vào URI thì mỗi tổ hợp đầu vào có thể có sẽ bị coi là một tài nguyên riêng

Phương thức làm rõ ngữ nghĩa giữa GET và POST

  • Nhiều triển khai truyền truy vấn trong phần thân POST thay vì dùng GET
    • POST /feed
    • Content-Type: application/x-www-form-urlencoded
    • phần thân: q=foo&limit=10&sort=-published
  • Cách này không cho thấy rõ đó có phải là truy vấn an toàn và idempotent hay không nếu không biết tài nguyên và máy chủ cụ thể
  • QUERY gửi cùng dữ liệu đầu vào trong phần thân yêu cầu, nhưng bản thân phương thức đã là an toàn và idempotent
    • QUERY /feed
    • Content-Type: application/x-www-form-urlencoded
    • phần thân: q=foo&limit=10&sort=-published
  • Nhờ ngữ nghĩa tường minh này, các tính năng HTTP như cache và tự động thử lại dễ được áp dụng hơn
  • Máy chủ có thể gán URI cho chính truy vấn hoặc cho kết quả của một truy vấn cụ thể để dùng cho các yêu cầu GET về sau

Các quy tắc cốt lõi của phương thức QUERY

  • QUERY được dùng để khởi tạo truy vấn phía máy chủ
  • GET yêu cầu biểu diễn của tài nguyên được định danh bởi URI đích, còn QUERY yêu cầu thực hiện thao tác truy vấn trong phạm vi của tài nguyên đích
  • Phần thân yêu cầu và media type xác định truy vấn, còn origin server quyết định phạm vi thao tác dựa trên tài nguyên đích
  • Máy chủ phải làm cho yêu cầu thất bại nếu thiếu trường yêu cầu Content-Type hoặc nếu nó không khớp với phần thân yêu cầu
  • Phần query của URI đích vẫn tham gia vào việc định danh tài nguyên đích như với mọi phương thức HTTP khác
    • Việc query part đó có ảnh hưởng trực tiếp đến kết quả hay không, và ảnh hưởng theo cách nào, là hành vi riêng của từng tài nguyên và nằm ngoài phạm vi đặc tả này
  • QUERY là an toàn theo góc nhìn của tài nguyên đích
    • Client không yêu cầu hay mong đợi thay đổi trạng thái của tài nguyên đích
    • Máy chủ vẫn có thể tạo ra các tài nguyên HTTP để tra cứu thêm thông tin, điều này không bị cấm
  • QUERY là idempotent nên có thể được thử lại hoặc lặp lại khi cần sau lỗi kết nối
  • Phản hồi 200 OK cho biết truy vấn đã được xử lý thành công và kết quả có trong phần thân phản hồi

Media type, đàm phán và xử lý lỗi

  • Ngữ nghĩa của yêu cầu QUERY thay đổi theo phần thân yêu cầu và metadata liên quan như media type
  • Những yêu cầu có phần thân và metadata không nhất quán thường phải bị từ chối bằng 4xx Client Error
  • Việc xử lý lỗi khác nhau tùy theo chỗ yêu cầu bị sai
    • Nếu không có thông tin media type thì yêu cầu sai theo định nghĩa, nên phải thất bại với mã trạng thái 4xx như 400
    • Nếu media type đã được chỉ định nhưng tài nguyên không hỗ trợ, thì 415 Unsupported Media Type là phù hợp
    • Ngay cả khi media type về nguyên tắc là đã biết, nếu tài nguyên đích không có ngữ nghĩa QUERY cho nó thì cũng thuộc trường hợp 415
    • Nếu media type không khớp với phần thân yêu cầu thực tế, có thể trả về 400 Bad Request
    • Máy chủ không được dùng content sniffing để suy đoán media type từ phần thân rồi ghi đè giá trị bị thiếu hoặc sai
    • Nếu kiểu và phần thân khớp nhau nhưng nội dung truy vấn thực tế vẫn không thể xử lý, có thể dùng 422 Unprocessable Content
    • Ví dụ 422 là một truy vấn SQL đúng cú pháp nhưng trỏ đến một bảng không tồn tại
    • Nếu tài nguyên không hỗ trợ media type phản hồi mà client yêu cầu bằng Accept, thì 406 Not Acceptable là phù hợp
  • Trường phản hồi Accept-Query có thể cho client biết các media type truy vấn được hỗ trợ

equivalent resource, Content-Location, Location

  • equivalent resource là tài nguyên đại diện cho một yêu cầu QUERY cụ thể và đối tượng của nó, đồng thời phản hồi cho yêu cầu GET
  • equivalent resource xét đến cả phần thân yêu cầu lẫn metadata
    • Bao gồm representation metadata như media type của phần thân
  • Máy chủ có thể gán URI cho equivalent resource nhưng không bắt buộc
  • Phản hồi thành công có thể bao gồm định danh của tài nguyên tương ứng với kết quả truy vấn trong header Content-Location
    • Client có thể gửi GET đến URI đó để lấy kết quả của thao tác truy vấn vừa thực hiện
    • Tài nguyên đó có thể là tạm thời
  • Phản hồi thành công có thể bao gồm URI của equivalent resource của yêu cầu QUERY trong header Location
    • Client có thể gửi GET đến URI đó để lặp lại cùng thao tác truy vấn mà không cần gửi lại phần thân truy vấn
    • URI này cũng có thể là tạm thời
    • Nếu các yêu cầu sau đó thất bại, client có thể thử lại với đích QUERY ban đầu và phần thân đã gửi trước đó

Chuyển hướng và yêu cầu có điều kiện

  • Máy chủ có thể chọn phản hồi gián tiếp để chuyển hướng user agent của yêu cầu QUERY sang URI khác
  • 301 Moved Permanently308 Permanent Redirect cho biết tài nguyên đích đã được chuyển vĩnh viễn sang URI khác do Location chỉ ra
  • 302 Found307 Temporary Redirect mang nghĩa chuyển tạm thời của tài nguyên đích
  • Trong cả bốn trường hợp, máy chủ gợi ý rằng có thể thực hiện yêu cầu gốc bằng cách gửi một yêu cầu QUERY tương tự đến URI đích mới
  • Ngoại lệ chuyển hướng POST thành GET sau 301 hoặc 302 không áp dụng cho yêu cầu QUERY
  • 303 See Other cho QUERY cho biết có thể thực hiện truy vấn gốc như một yêu cầu tra cứu thông thường với URI do Location chỉ ra
    • Trong HTTP, điều đó có nghĩa là gửi yêu cầu GET đến URI đích mới
  • Trong QUERY có điều kiện, selected representation giống với GET trên equivalent resource của chính yêu cầu QUERY đó
  • Client có thể yêu cầu chỉ trả về kết quả truy vấn nếu các điều kiện do header điều kiện chỉ định được thỏa mãn

Cache và yêu cầu Range

  • Phản hồi của phương thức QUERY có thể được cache, và cache có thể dùng để đáp ứng các yêu cầu QUERY sau này
  • Khóa cache của yêu cầu QUERY phải bao gồm phần thân yêu cầu và metadata liên quan
  • Cache có thể loại bỏ các khác biệt không quan trọng về mặt ngữ nghĩa khi tạo khóa cache
    • loại bỏ content encoding
    • chuẩn hóa theo quy ước định dạng do hậu tố media subtype như +json biểu thị
    • chuẩn hóa theo ngữ nghĩa phần thân mà Content-Type biểu thị
  • Những biến đổi này chỉ nhằm tạo khóa cache và không làm thay đổi chính yêu cầu
  • Client có thể dùng chỉ thị cache no-transform để biểu thị không muốn các biến đổi này, nhưng chỉ thị này chỉ mang tính advisory
  • Việc cache phản hồi QUERY vốn phức tạp hơn GET
    • Để xác định khóa cache, phải đọc toàn bộ phần thân yêu cầu
    • Nếu phản hồi QUERY cung cấp URI equivalent resource qua Location, client có thể chuyển sang GET ở các lần sau để đơn giản hóa xử lý
  • Ngữ nghĩa của Range Request với QUERY giống như với GET
  • Byte Range Request, là range unit duy nhất được định nghĩa tại thời điểm viết, ít có giá trị với kết quả QUERY
  • Trong nhiều trường hợp, bản thân ngôn ngữ truy vấn đã cung cấp cách giới hạn kết quả hoặc phân trang như FETCH FIRST ... ROWS ONLY của SQL, và dự kiến sẽ dùng các khả năng tích hợp đó

Header phản hồi Accept-Query

  • Header phản hồi Accept-Query trực tiếp thông báo rằng tài nguyên hỗ trợ phương thức QUERY và xác định các media type của định dạng truy vấn có thể dùng
  • Accept-Query là một danh sách media range dùng cú pháp Structured Fields
  • Media range được biểu diễn bằng List Structured Header Field gồm Token hoặc String chứa giá trị media range không có tham số
  • Các tham số media type được ánh xạ thành Structured Field Parameters
    • Việc chọn String hay Token không quan trọng về mặt ngữ nghĩa
    • Bên nhận có thể chuyển Token thành String, nhưng không được xử lý khác nhau chỉ vì kiểu nhận được khác nhau
  • Media type không ánh xạ chính xác với Token; nếu cần cho phép chữ số đứng đầu thì phải dùng dạng String
  • Wildcard được hỗ trợ chỉ là */* hoặc xxxx/*
  • Thứ tự các kiểu được liệt kê trong giá trị trường không quan trọng
  • Giá trị Accept-Query áp dụng cho mọi URI của máy chủ có cùng path, còn query component bị bỏ qua
  • Nếu cùng một yêu cầu tài nguyên trả về các giá trị Accept-Query khác nhau, thì dùng giá trị fresh nhận được gần nhất
  • Ví dụ như sau
    • Accept-Query: "application/jsonpath", application/sql;charset="UTF-8"
  • Accept-Query trông giống Accept, nhưng vì là Structured Field nên phải tuân theo các quy tắc xử lý Structured Fields của RFC 9651

Cân nhắc bảo mật và CORS

  • QUERY tuân theo mọi cân nhắc bảo mật chung cho các phương thức HTTP được định nghĩa trong RFC 9110
  • QUERY có thể được dùng như một lựa chọn thay thế cho cách đặt thông tin yêu cầu trong query component của URI
  • URI có nhiều khả năng bị ghi log hoặc bị các bên trung gian xử lý hơn phần thân yêu cầu, nên với các truy vấn chứa thông tin nhạy cảm, đây có thể là động lực để dùng QUERY thay vì GET
  • Khi máy chủ tạo tài nguyên tạm đại diện cho kết quả QUERY và gán URI cho nó, nếu phần thân yêu cầu gốc có chứa thông tin nhạy cảm không nên bị ghi log thì URI đó không được chứa các phần nhạy cảm
  • Nếu cache chuẩn hóa sai phần thân QUERY hoặc chuẩn hóa quá khác với cách tài nguyên xử lý, nó có thể trả về phản hồi sai do false positive
  • Yêu cầu QUERY từ user agent triển khai CORS cần có yêu cầu preflight
    • QUERY không nằm trong tập CORS-safelisted methods

Đăng ký IANA và lựa chọn tên phương thức

  • IANA thêm phương thức QUERY vào HTTP Method Registry
    • Method Name: QUERY
    • Safe: yes
    • Idempotent: yes
    • Specification: RFC 10008 Section 2
  • IANA thêm trường Accept-Query vào HTTP Field Name Registry
    • Field Name: Accept-Query
    • Status: permanent
    • Structured Type: List
  • Trong HTTP Method Registry đã có các phương thức PROPFIND, REPORT, SEARCH với thuộc tính safe và idempotent
  • Ở giai đoạn đầu người ta dùng SEARCH, nhưng tên phương thức cuối cùng là QUERY
  • Lý do chọn QUERY như sau
    • Các phương án thay thế dùng media type tổng quát application/xml trong phần thân yêu cầu và ngữ nghĩa yêu cầu phụ thuộc hoàn toàn vào phần thân
    • Các phương án thay thế đều bắt nguồn từ hoạt động WebDAV
    • QUERY nắm bắt tốt mối quan hệ với query component của URI

1 bình luận

 
Ý kiến trên Hacker News
  • Sẽ thuyết phục hơn nhiều nếu có ví dụ tạo động lực mạnh; thay vào đó lại dùng ví dụ quá dễ biểu đạt bằng GET nên thành ra khá rối
    Ngay cả khi hình dung QUERY với cấu trúc bộ lọc JSON lớn hoặc đầu vào hình ảnh được đặt trong phần thân yêu cầu, việc phần thân yêu cầu trở thành một phần của khóa cache vẫn tạo cảm giác rất kỳ lạ. Điều đó tạo ra các khóa cache vô hạn do người dùng kiểm soát, và các chiến lược cache thông thường thực tế chỉ còn cách so sánh phần thân yêu cầu từng bit hoặc băm nó, nên trong tình huống ác ý thì việc làm vô hiệu cache trở nên rất dễ
    Nếu đang xây một dịch vụ cần lọc phức tạp hoặc đầu vào phức tạp như hình ảnh, thì cache nhiều khả năng sẽ nằm ở nơi khá xa tầng HTTP. Ví dụ như embedding được khóa theo từng cột dữ liệu riêng của phép join, hoặc theo perceptual hash của đầu vào hình ảnh đã giải mã, tức là không phụ thuộc vào biểu diễn bit chính xác trên đường truyền
    Tôi không hiểu vì sao lại cố nắm bắt những thứ này theo một cách dùng chung. Thà biểu đạt ngữ nghĩa cache bằng một header mới như "Vary: request-body" cho POST còn tốt hơn nhiều. Hoàn toàn tương thích ngược, và ngoài 0,1% trường hợp dùng CDN nơi hành vi này có thể hữu ích thì có thể bỏ qua

    • Phần query trong URI của GET trên thực tế cũng gần như không bị giới hạn, do người dùng kiểm soát, và vì là một phần của URI nên cũng đi vào khóa cache. Vì vậy tôi không rõ vì sao lập luận ngược lại lại bị nêu ra như một điều đặc biệt
    • Nếu trình duyệt muốn khóa cache nhỏ hơn thì chỉ cần lưu hàm băm chống va chạm của phần thân. Ví dụ dùng SHA-256 là được
      Tôi không nghĩ ra kiểu tấn công nào liên quan đến cache mà không thể áp dụng y hệt cho tham số truy vấn. Nếu muốn làm tràn cache, tạo một tham số truy vấn 30 ký tự duy nhất cũng dễ như tạo một phần thân yêu cầu 30MB
    • Không phải mọi trường hợp sử dụng đều là Internet công cộng, và việc không hữu ích trên Internet công cộng cũng không có nghĩa là không thể tiêu chuẩn hóa
      Thực tế thì các hệ thống dùng cho Internet công cộng sẽ dùng hàm băm bảo mật làm khóa cache để luôn có cùng kích thước. Khóa cache vốn đã bao gồm URL có thể rất dài và cả một tập giá trị header tùy ý
    • Có thể gửi hình ảnh trong phần thân yêu cầu, nhưng cũng đã có thể làm điều đó bằng tham số truy vấn base64. Nếu cố tình dùng kỳ quặc thì bất kỳ chuẩn đề xuất nào cũng có thể bị dùng tệ
      GET có tham số truy vấn vốn đã mờ đục và khiến việc vô hiệu cache trở nên dễ dàng
    • Ví dụ, hiện tôi đang làm một máy chủ MCP cho cơ sở dữ liệu. Trong ChatGPT, tôi muốn thử dry-run POST trước, rồi rollback trước khi commit, nhưng cả hai đều là yêu cầu POST chỉ khác đúng một thuộc tính nên thường xuyên đụng vào tầng an toàn của công cụ. Vì nhiều lý do, việc debug nguyên nhân chính xác cũng khó
      Nhưng nếu dùng QUERY rồi sau đó là POST thì có vẻ sẽ tốt hơn. Vì khi đó chúng là các loại yêu cầu khác nhau, chứ không chỉ là cùng một yêu cầu có gắn cờ an toàn
  • Tôi tự hỏi liệu form HTML có bổ sung hỗ trợ QUERY hay không
    QUERY phải là idempotent, nên có thể tránh được cảnh báo gửi lại khó chịu khi làm mới trang kết quả của một lần gửi form POST

    • Hỗ trợ nhiều phương thức hơn GET/POST trong form HTML là điều người ta mong suốt nhiều thập kỷ. Đúng lúc cũng có đề xuất WHATWG, nên nếu muốn góp tiếng nói thì vào đây: https://github.com/whatwg/html/pull/11347
    • Một điểm kỳ lạ của form là kết quả của form POST là một trang có vị trí (URL), nhưng lại không thể tải bằng chính vị trí đó. Theo tôi biết, việc trang đó là POST chứ không phải GET không được lưu ở đâu mà người dùng hay JS có thể thấy, và hành vi làm mới cũng rất kỳ quặc
      Nếu thêm method=QUERY thì sẽ chỉ tạo thêm một biến thể kỳ quặc mới của chuyện này
    • Cách tốt hơn là giải quyết bằng mẫu POST-Redirect-GET
    • Xem https://github.com/whatwg/html/issues/12594
    • Họ chưa từng thêm hỗ trợ cho các động từ khác, nhưng giờ là thời đại mới nên cũng không biết sẽ ra sao
  • Dành cho những ai vẫn muốn giả vờ đang sống ở thế kỷ trước: https://www.rfc-editor.org/rfc/rfc10008.txt

    • Tôi có lẽ sẽ mãi thích những tài liệu văn bản thuần túy dài và đầy đủ như thế này. Nó gợi nhớ thời đẹp đẽ ngày bé đọc FAQ game. Xét trên nhiều phương diện, đây thực sự là một định dạng thông tin vượt trội
    • Định dạng thật đẹp. Chắc tôi sẽ chép lại làm mẫu phong cách cho memo công việc nội bộ. Không lỗi thời
  • “Phương án gắn phần thân vào yêu cầu GET đã được nhóm làm việc IETF xem xét sâu, nhưng cuối cùng họ quyết định tạo phương thức QUERY mới. Quyết định tạo phương thức riêng là do các vấn đề tương tác lịch sử và việc tuân thủ nghiêm ngặt định nghĩa kiến trúc cốt lõi của HTTP”
    Thế nhưng nhiều năm nay tôi vẫn gửi phần thân yêu cầu trong phương thức GET

    • Có người nói một số bộ cân bằng tải sẽ bỏ phần thân đi
    • Nói chung đó không phải ý hay. Một số triển khai HTTP thậm chí không làm được. Ví dụ fetch là như vậy
      https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/U...
      “Yêu cầu GET không thể bao gồm phần thân”
      Do cache trong suốt nên cũng có thể phát sinh vấn đề kỳ quặc
  • Thật ngạc nhiên là giờ đã tới số RFC 5 chữ số rồi

  • Có người đặt một kèo dự đoán mơ hồ về thời điểm RFC 10000 được phát hành, nhưng số hiệu lại nhảy từ 9998 thẳng lên 10008. Không ai thắng cả
    https://manifold.markets/CollectedOverSpread/when-will-rfc-1...

  • Nếu truy vấn kết quả tìm kiếm trong HTTP thì giờ sẽ thành dùng phương thức QUERY, chứ không thêm tham số truy vấn nữa
    Cái tên này gây bối rối. Vì thuật ngữ query vốn đã được dùng để chỉ yêu cầu HTTP nói chung
    Chỉ nhìn tiêu đề RFC thôi tôi đã thấy rối rồi

    • Thuật ngữ query dùng để chỉ yêu cầu HTTP nói chung là trong lĩnh vực nào? Trong lời nói thường ngày thì đôi khi người ta gọi yêu cầu GET là truy vấn, nhưng với POST, PUT, DELETE thì không bao giờ gọi vậy
    • Đúng. Hơn nữa cũng không nhất thiết phải là truy vấn, mà có thể là hiệu ứng idempotent. Có lẽ gọi là IPOST, tức POST idempotent, còn hợp hơn
      Sửa: à, vì khả năng cache nên QUERY được tuyên bố là một phương thức “an toàn” không có tác dụng phụ. Tôi đã nhầm
  • Nếu điều này thực sự thay thế các yêu cầu GET kèm query string ngoài thực tế, thì tôi rất mong bookmark của trình duyệt sẽ hỗ trợ giữ lại tham số yêu cầu

    • Có lẽ sẽ không thế đâu. Khả năng lớn là nó sẽ thay thế những chỗ hiện đang dùng POST cho mục đích truy vấn
  • Tôi biết điều này nằm ngoài phạm vi của RFC này, nhưng điều hay là nếu mở rộng nó một cách đơn giản thì có thể khiến JS EventSource hoạt động cả với truy vấn AI dạng streaming
    Vì yêu cầu cần có phần thân nên mọi người đều dùng POST, còn kết quả streaming thì thường dùng giao thức text/event-stream trong phản hồi. Nhưng thực ra không có trạng thái nào thay đổi, nên về mặt kỹ thuật nó không thật sự khớp, trong khi EventSource lại cứng đầu chỉ dùng được GET. Vì thế nhiều API phải tự triển khai lại cùng chức năng bằng parser riêng

  • Khi thấy GET: Content (body) "no defined semantics", tôi từng nghĩ có lẽ cho phép phần thân trong phương thức GET cũng không sao, nhưng theo đặc tả gốc thì phần thân của GET phải bị bỏ qua hoàn toàn
    Ngoài ra, nếu phần quan trọng của yêu cầu nằm trong phần thân có thể bị loại bỏ thì việc cache cũng có thể hỏng

    • GET chỉ có URI mang ngữ nghĩa là lấy biểu diễn hiện tại của tài nguyên. Đây là hình thức cơ bản nhất của hyperlink và khá quan trọng với cách web vận hành
      Nếu thêm tham số phần thân vào GET, thì hai yêu cầu dùng cùng một URI sẽ không còn có thể được xem là đang trỏ tới cùng một thứ nữa, và điều đó phá vỡ các ràng buộc của phương thức này