Metode HTTP QUERY, dan siapa yang benar-benar mendukungnya

Bagikan

Pada bulan Juni, IETF menerbitkan RFC 10008, yang mendefinisikan metode HTTP baru bernama QUERY. Metode terakhir yang distandarkan adalah PATCH, pada 2010. QUERY bekerja seperti GET yang membawa body: aman, idempoten, dan bisa di-cache, dan query-nya ada di body request, bukan di URL.

Kedengarannya seperti sesuatu yang seharusnya sudah didukung semua orang, jadi kami mengujinya. Kami mengirim request QUERY ke lima API uji publik, termasuk milik kami, dan mendapat lima response yang berbeda. Hanya satu yang menjalankan query-nya. Kami juga mencoba Node, ASP.NET Core, dan Python, dan ketiganya menanganinya tanpa masalah. Di kode aplikasi, metode ini sudah siap. Proxy dan server di sepanjang jalan lain cerita.

Masalah yang diselesaikan QUERY

Misalkan Anda punya pencarian produk dengan selusin filter, rentang harga bersarang, dan urutan sortir. Saat ini Anda punya dua pilihan, dan keduanya punya kekurangan.

  • GET dengan semuanya di query string. Anda harus meng-encode filter bersarang ke URL entah bagaimana. URL panjang membentur batas panjang di server dan proxy, dan seluruh query berakhir di log akses, riwayat browser, dan header Referer. Memasang body pada GET tidak membantu: RFC 9110 menyatakan konten dalam request GET tidak punya makna yang ditetapkan, dan banyak proxy serta klien membuangnya.
  • POST /search. Inilah yang akhirnya dilakukan kebanyakan API. Berhasil, tetapi POST memberi tahu setiap klien dan proxy bahwa request itu mungkin mengubah sesuatu. Jadi tidak ada yang mengulangnya otomatis setelah koneksi terputus, dan tidak ada cache yang menyimpan hasilnya.

QUERY memberi Anda body request yang tetap bisa diperlakukan klien, proxy, dan cache seperti GET:

GETPOSTQUERY
Body request punya makna yang ditetapkanTidakYaYa
Aman (tidak mengubah apa pun)YaTidakYa
Idempoten (aman diulang)YaTidakYa
Response bisa di-cacheYaJarangYa, dengan body sebagai kunci
Query berakhir di URL dan logYaTidakTidak

Seperti apa request QUERY

Ia terlihat seperti POST, hanya saja nama metodenya memberi tahu semua pihak di jalan bahwa ia hanya membaca:

Request
QUERY /products/search HTTP/1.1
Host: shop.example
Content-Type: application/json
Accept: application/json

{"category":"electronics","maxPrice":500,"inStock":true,"sort":"price"}
Response
HTTP/1.1 200 OK
Content-Type: application/json
Location: /products/search/q/7f3a9c
Cache-Control: max-age=60

[{"id":4,"name":"USB-C hub","price":39},{"id":11,"name":"Mechanical keyboard","price":89}]

Header Location bersifat opsional, dan merupakan salah satu bagian RFC yang paling berguna. Ia memberi URL yang menjalankan query yang sama dengan GET biasa, jadi klien cukup mengirim body sekali lalu memakai tautan biasa sesudahnya. Content-Location artinya sedikit berbeda: URL yang mengembalikan hasil yang satu ini. Server juga bisa membalas 303 See Other tanpa hasil sama sekali, yang menyuruh klien mengambilnya dengan GET dari URL di Location.

Yang diminta RFC dari server

Sebagian besar soal ketat terhadap body, karena body-nya adalah query:

  • Content-Type wajib. Jika tidak ada, server menolak request dengan 4xx, biasanya 400. Server tidak boleh menebak tipenya dari body.
  • Format yang tidak diterima resource untuk query mendapat 415. Body yang tidak cocok dengan tipe yang dinyatakan mendapat 400, dan query yang formatnya benar tetapi tidak bisa dijalankan server mendapat 422.
  • Header response baru, Accept-Query, mendaftar format query yang diterima sebuah resource. Ia juga memberi tahu klien bahwa QUERY didukung:
Response
HTTP/1.1 415 Unsupported Media Type
Accept-Query: application/json, "application/jsonpath"
  • Cache boleh menyimpan response QUERY, tetapi kunci cache harus menyertakan body request. Dua query ke URL yang sama dengan filter berbeda adalah dua entri cache yang berbeda.
  • Setelah redirect 301 atau 302, klien mengirim QUERY lagi ke URL baru. Klien sering mengubah POST menjadi GET setelah redirect semacam itu, dan RFC melarangnya untuk QUERY.

Kami mengirim QUERY ke lima API uji publik

API uji dibuat untuk mengembalikan apa pun yang Anda kirim, jadi wajar mengira mereka adalah server paling toleran yang ada. Ini skripnya:

probe-query.mjs
// Send one QUERY request to each URL and report what came back.
const targets = [
  "https://httpbin.org/anything",
  "https://httpbingo.org/anything",
  "https://postman-echo.com/query",
  "https://jsonplaceholder.typicode.com/posts",
  "https://api.testingapis.com/api/methods/any",
];

for (const url of targets) {
  const res = await fetch(url, {
    method: "QUERY", // capitals: fetch only uppercases the six standard methods
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ category: "electronics", maxPrice: 500 }),
  });
  const type = (res.headers.get("content-type") ?? "-").split(";")[0];
  const allow = res.headers.get("allow") ?? "-";
  console.log(new URL(url).host.padEnd(29), res.status, type.padEnd(17), "allow:", allow);
}
Output (Node 24, 10 Oktober 2026)
httpbin.org                   405 text/html         allow: HEAD, POST, TRACE, PUT, GET, OPTIONS, DELETE, PATCH
httpbingo.org                 200 application/json  allow: -
postman-echo.com              400 text/plain        allow: -
jsonplaceholder.typicode.com  404 application/json  allow: -
api.testingapis.com           400 text/plain        allow: -

Lima server, lima jawaban berbeda:

APIResponseYang disampaikannya ke klien
httpbingo.org200 dengan metode dan body dikembalikanBerhasil. httpbingo adalah tulis ulang httpbin dalam Go, dan net/http milik Go meloloskan metode apa pun.
httpbin.org405 dengan header AllowCara yang benar untuk menolak: metodenya tidak didukung, dan ini daftar yang didukung.
jsonplaceholder.typicode.com404Menyesatkan. URL-nya ada, tetapi router tidak menemukan rute, jadi klien akan mengira path-nya salah.
postman-echo.com400, 11 byte text/plainMenyatakan request-nya rusak, padahal tidak.
api.testingapis.com400, 11 byte text/plainSama saja, dan yang ini milik kami.

Begini API kami sendiri gagal:

Terminal
$ curl -i -X QUERY https://api.testingapis.com/api/methods/any \
    -H "content-type: application/json" -d '{"q":"x"}'
HTTP/1.1 400 Bad Request
content-length: 11
content-type: text/plain
connection: close

Bad Request

Response ini tidak pernah sampai ke aplikasi kami. Setiap response dari aplikasi ASP.NET Core kami membawa header server: Kestrel dan x-powered-by: TestingAPIs, dan yang ini tidak punya keduanya. API-nya berjalan di Azure Container Apps, dan proxy ingress di depannya menolak metode itu sebelum meneruskannya. Postman Echo mengembalikan body 11 byte yang sama dan header x-envoy-upstream-service-time, jadi tampaknya hal yang sama terjadi di sana, di belakang Envoy. Kami mendapat 400 yang sama untuk metode karangan seperti FOO, jadi proxy memperlakukan QUERY sebagai metode yang tidak dikenal, bukan request yang buruk.

Pelajarannya soal di mana masalahnya. Framework Anda mungkin menangani QUERY dengan baik sementara load balancer, ingress, atau WAF di depannya menolak request, dan error yang kembali tidak memberi petunjuk apa pun. Jadi uji QUERY melalui setiap hop yang dilalui klien sungguhan, bukan hanya terhadap localhost.

Framework sudah mendukungnya

Kode aplikasi jauh lebih siap. Kami mencoba tiga server di mesin tempat kami menulis ini, dan masing-masing menangani QUERY tanpa pengaturan tambahan.

Node.js 24. Parser HTTP bawaannya mengenal metode ini, dan fetch bisa mengirimnya:

server.mjs
import http from "node:http";

console.log(http.METHODS.includes("QUERY")); // true on Node 24

http
  .createServer((req, res) => {
    if (req.method !== "QUERY") {
      res.writeHead(405, { allow: "QUERY" }).end();
      return;
    }
    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      const filters = JSON.parse(body);
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({ method: req.method, filters }));
    });
  })
  .listen(3000);

ASP.NET Core 10. .NET 10 menyertakan HttpMethods.Query di sisi server dan HttpMethod.Query untuk HttpClient:

Program.cs
using System.Net.Http.Json;

var app = WebApplication.Create(args);

app.MapMethods("/products/search", [HttpMethods.Query], async (HttpRequest req) =>
{
    var filters = await req.ReadFromJsonAsync<ProductFilter>();
    return Results.Ok(new { method = req.Method, filters });
});

app.Urls.Add("http://localhost:5000");
await app.StartAsync();

// Call it with HttpClient, then try a GET on the same route.
using var http = new HttpClient();
var msg = new HttpRequestMessage(HttpMethod.Query, "http://localhost:5000/products/search")
{
    Content = JsonContent.Create(new { category = "electronics", maxPrice = 500 })
};
var res = await http.SendAsync(msg);
Console.WriteLine($"QUERY: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");

var get = await http.GetAsync("http://localhost:5000/products/search");
Console.WriteLine($"GET:   {(int)get.StatusCode} Allow: {string.Join(", ", get.Content.Headers.Allow)}");

await app.StopAsync();

record ProductFilter(string Category, decimal MaxPrice);
Output (.NET 10)
QUERY: 200 {"method":"QUERY","filters":{"category":"electronics","maxPrice":500}}
GET:   405 Allow: QUERY

Perhatikan baris kedua. GET ke rute yang hanya menangani QUERY mendapat 405 dengan Allow: QUERY tanpa Anda menulis apa pun untuk itu. Itu penolakan sopan yang sama dengan yang diberikan httpbin.org.

Python. http.server meneruskan setiap request ke handler do_<METHOD>, jadi menambahkan QUERY cukup dengan satu metode. Di sisi klien, urllib.request.Request menerima method="QUERY".

server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class Handler(BaseHTTPRequestHandler):
    def do_QUERY(self):
        length = int(self.headers.get("content-length", 0))
        filters = json.loads(self.rfile.read(length))
        body = json.dumps({"method": self.command, "filters": filters}).encode()
        self.send_response(200)
        self.send_header("content-type", "application/json")
        self.end_headers()
        self.wfile.write(body)

HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()

Tulis dengan huruf kapital

Nama metode HTTP peka huruf besar-kecil. Standar fetch mengubah enam di antaranya menjadi huruf kapital untuk Anda: DELETE, GET, HEAD, OPTIONS, POST, dan PUT. Selebihnya dikirim persis seperti yang Anda ketik. Ini yang dikembalikan server Node untuk tiga penulisan:

Output (Node 24)
"query" -> 400
"QUERY" -> 200 QUERY
"post" -> 200 POST

method: "query" dikirim sebagai query huruf kecil, dan server menjawab 400. PATCH punya jebakan yang sama selama bertahun-tahun, dan Node menampilkan peringatan saat Anda menulis "patch". Untuk "query" ia tidak memperingatkan. Jika request QUERY gagal dan Anda tidak tahu sebabnya, periksa huruf kapitalnya.

Browser mengirim preflight lebih dulu

QUERY tidak ada di daftar pendek metode yang dikirim browser lintas origin tanpa bertanya dulu. Jadi sebelum QUERY lintas origin, browser mengirim preflight OPTIONS, dan server harus menyebut QUERY di Access-Control-Allow-Methods. httpbingo adalah satu-satunya API yang menjalankan query kami, dan inilah jawabannya untuk browser:

Terminal
$ curl -i -X OPTIONS https://httpbingo.org/anything \
    -H "Origin: https://example.com" \
    -H "Access-Control-Request-Method: QUERY" \
    -H "Access-Control-Request-Headers: content-type"
HTTP/1.1 200 OK
access-control-allow-origin: https://example.com
access-control-allow-methods: GET, POST, HEAD, PUT, DELETE, PATCH, OPTIONS
access-control-allow-headers: content-type

QUERY tidak ada di daftar itu, jadi request yang sama yang berhasil dari Node akan gagal di browser dengan error CORS. Jika pengaturan CORS Anda mendaftar metode yang diizinkan berdasarkan nama, tambahkan QUERY juga.

OpenAPI 3.2 bisa mendeskripsikannya

OpenAPI 3.2, yang rilis September 2025, menambahkan operasi query ke path item, di samping get dan post:

openapi.yaml
openapi: 3.2.0
info:
  title: Shop
  version: 1.0.0
paths:
  /products/search:
    query:
      summary: Search products with filters in the body
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProductFilter"
      responses:
        "200":
          description: Matching products

Apakah generator dokumentasi, generator klien, atau gateway API Anda sudah membaca field itu adalah soal lain. Periksa sebelum Anda mengandalkannya, sama seperti dengan jaringan.

Haruskah Anda sudah memakai QUERY?

Untuk lalu lintas antarlayanan Anda sendiri, di mana Anda mengendalikan setiap hop, ya. Uji jalur lengkapnya sekali dan Anda mendapat retry dan caching yang tidak bisa diberikan POST /search.

Untuk API publik, kami akan menambahkannya tanpa menghilangkan apa pun:

  • Biarkan POST /search tetap berfungsi dan tambahkan QUERY pada resource yang sama. Klien dan jaringan yang tidak bisa mengirim QUERY tetap punya jalan masuk.
  • Kirim Accept-Query agar klien tahu QUERY didukung sebelum mencobanya.
  • Kembalikan header Location. Setelah klien menjalankan sebuah query, ia bisa mengulanginya dengan GET, yang lolos melalui proxy mana pun.
  • Di rute yang tidak mendukung QUERY, jawab 405 dengan header Allow. Jangan jawab 400 atau 404, yang menyiratkan masalah pada request klien.

Jika Anda memeriksa cara klien Anda menangani server yang menolak sebuah metode, pelajaran metode HTTP kami membahas 405 dan Allow, dan panduan response errormenunjukkan cara mendapatkan response 4xx dan 5xx dari API kami kapan saja.

PATCH butuh bertahun-tahun untuk bekerja andal di berbagai klien, proxy, dan framework, dan QUERY mungkin juga butuh waktu. Bedanya kali ini, kode aplikasi sudah siap sejak hari pertama. Yang masih gagal adalah proxy-nya.