متد QUERY در HTTP: وقتی GET و POST هیچکدوم جواب نمیدن
یه سوال قدیمی توی طراحی API وجود داشت که همه باهاش کنار اومده بودن: وقتی میخوای یه query پیچیده بفرستی و چیزی رو بخوانی — نه تغییر بدی — چه متدی استفاده میکنی؟ GET بدنه نداره، POST هم از نظر پروتکل یعنی «دارم یه چیزی رو تغییر میدم». نتیجه؟ همه یه endpoint مثل POST /search میساختن و ادامه میدادن.
ژوئن ۲۰۲۶، IETF این مشکل رو رسمی حل کرد. RFC 10008 تاریخ ۱۵ ژوئن ۲۰۲۶ منتشر شد و متد جدیدی به اسم QUERY رو برای HTTP تعریف میکنه. این اولین متد کاملاً جدید HTTP در حدود ۱۶ سال گذشته است.
مشکل واقعی چی بود
GET برای درخواستهای read-only ایدهآله، ولی یه محدودیت بزرگ دارد: همه چیز باید توی URL باشد. برای یه فیلتر ساده مثل ?city=Berlin&limit=50 کافیه، ولی وقتی query پیچیده میشود — مثلاً یه فیلتر تو در تو با چند شرط، یا یه کوئری SQL کامل — URL به سرعت به جایی میرسد که دیگر نمیتوان باهاش کار کرد. RFC 9110 فقط توصیه میکند که پیادهسازیها حداقل ۸۰۰۰ بایت URI را پشتیبانی کنند، و این یه کف است، نه یه تضمین. محدودیت واقعی را وقتی کشف میکنی که پشت یه پراکسی یا لود بالانسر قدیمی هستی — و test suiteات آن را نشان نمیدهد.
POST هم یه راهحل دیگر بود، ولی از نظر پروتکل HTTP مشکل داشت.POST نه safe هست و نه idempotent. وقتی connection وسط درخواست قطع میشود، نه client نه هیچ پراکسیای نمیتواند بداند که آیا state روی سرور عوض شده یا نه. در نتیجه هیچچیزی بهطور خودکار retry نمیکنه. علاوه بر این، cache هم برای POST کار نمیکند — هر بار باید به origin برگردد.
یه مشکل ظریفتر هم بود که کمتر بهش توجه میشد: URL ها leak میکنند. request URIها بیشتر از request body توی logها ثبت میشوند. اگه query ات اطلاعات حساسی دارد، الان توی هر access log در طول مسیر است.
QUERY چطور این مشکلها رو حل میکنه
QUERY دقیقاً همان چیزی است که GET و POST هیچکدام نبودند: یه درخواست safe، idempotent و cacheable که بدنه هم دارد.
QUERY /contacts HTTP/1.1
Content-Type: application/json
{
"filter": { "city": "Berlin" },
"limit": 50
}
این یعنی body خود query است — نه یه resource که باید ذخیره شود، نه یه command. سرور آن را پردازش میکند و نتیجه را برمیگرداند.
مقایسهی سه متد از نظر پروتکل:
| ویژگی | GET | QUERY | POST |
|---|---|---|---|
| Safe | آره | آره | ❌ |
| Idempotent | آره | آره | ❌ |
| Cacheable | آره | آره | محدود |
| Request body | ندارد | دارد | دارد |
یه مکانیزم discovery هم اضافه شده: سرور میتواند با هدر جدید Accept-Query اعلام کند که چه media typeهایی را برای QUERY قبول میکند:
Accept-Query: "application/json", application/sql
کش کردن چطور کار میکنه
QUERY قابل cache است، ولی کمی پیچیدهتر از GET. cache key باید شامل محتوای request هم باشد، نه فقط URI. کش باید ابتدا کل body را بخواند و بعد تصمیم بگیرد آیا قبلاً این درخواست را دیده یا نه.
برای جلوگیری از missهای بیدلیل، cacheها اجازه دارند تفاوتهای بیمعنی را نرمالایز کنند — مثلاً ترتیب فیلدهای JSON. ولی اگه این نرمالایزیشن خیلی aggressive باشد، ممکن است cache جواب اشتباه را برگرداند. این edge case اصلیایه که باید بهش توجه داشت.
چه کسی الان پشتیبانی میکنه
Node.Js از اوایل ۲۰۲۴ متد QUERY را بهصورت native parse میکند، OpenAPI 3.2 میتواند آن را document کند، و Spring یه pull request باز دارد. وضعیت دقیقتر به این شکل است:
| Stack | وضعیت (جولای ۲۰۲۶) |
|---|---|
| Node.js | ✅ Native — از Node 21.7.2 و Node 22+ |
| Express | کار میکند، ولی TypeScript type ندارد |
| Fastify | opt-in با addHttpMethod('QUERY', { hasBody: true }) |
| Go net/http | بدون constant، ولی method string کار میکند |
| Spring | ❌ PR باز دارد، هنوز merge نشده |
| ASP.NET Core | ✅ در .NET 11 Preview 4 پشتیبانی میشود |
| OpenAPI | ✅ از نسخه ۳.۲.۰ (سپتامبر ۲۰۲۵) |
| curl | با -X QUERY --data و Content-Type |
| مرورگر (fetch) | قبول میکند، ولی همیشه CORS preflight میزند |
یه نکته مهم درباره مرورگر: QUERY یه CORS-safelisted method نیست، پس هر درخواست cross-origin یه preflight میزند. هزینهی این OPTIONS اضافه را باید در نظر بگیری.
الان چطور امتحانش کنی
با curl و Node 22 همین الان میشود یه endpoint کاملاً کارکرد ساخت:
curl -X QUERY 'http://localhost:3000/contacts' \
-H 'Content-Type: application/json' \
-d '{"filter": {"city": "Berlin"}, "limit": 50}'
سمت سرور روی Node، req.method مقدار 'QUERY' را برمیگرداند:
import { createServer } from 'node:http';
const server = createServer((req, res) => {
if (req.method !== 'QUERY') {
res.writeHead(405, { Allow: 'QUERY' });
res.end();
return;
}
let body = '';
req.on('data', chunk => (body += chunk));
req.on('end', () => {
const query = JSON.parse(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ query, results: [] }));
});
});
server.listen(3000);
از fetch هم همینطور کار میکند:
const res = await fetch('http://localhost:3000/contacts', {
method: 'QUERY',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filter: { city: 'Berlin' }, limit: 50 }),
});
اگه روی Spring هستی، فعلاً باید صبر کنی یا یه servlet filter دستی بنویسی که درخواستهای QUERY را پیش از Spring MVC بگیرد — ولی این یه راهحل موقتی است.
قبل پایانم این واژه هارو هم گفتم توضیح بدم که چی هستند:
Preflight Request:
درخواست اولیهای از نوع OPTIONS که مرورگر قبل از ارسال درخواست اصلی در CORS میفرستد تا بررسی کند سرور اجازه دسترسی میدهد یا نه.
Safe Methods:
متدهایی که فقط برای خواندن داده استفاده میشوند و نباید هیچ تغییری در state سرور ایجاد کنند؛ مثل GET, HEAD, OPTIONS.
Idempotent Methods:
متدهایی که اگر چند بار پشت سر هم اجرا شوند، نتیجه نهایی یکسانی روی سرور دارند؛ مثل GET, PUT, DELETE.
پس باید الان ازش استفاده کنی؟
برای سرویسهای داخلی روی Node، من همین الان استفاده میکنم. parser support وجود دارد، semantics استاندارد شده، و ترافیک داخلی نیازی به نگرانی از CDN یا بلوغ مرورگر ندارد.
برای API عمومی، عجله نکن. endpointهای POST فعلیات را نگه دار و Accept-Query را اضافه کن تا clientها بدانند که QUERY هم پشتیبانی میشود. tooling داره میآید — OpenAPI 3.2 و ASP.NET 11 هر دو آن را میشناسند.
ده یازده سال از اولین draft تا استاندارد شدن طول کشید. HTTP سریع حرکت نمیکند، و دقیقاً به همین خاطر وقتی حرکت میکند ارزش توجه دارد.سرچ کردن با متد POST احتمالاً ظرف چند سال آینده به همان اندازهای که الان X- header استفاده کردن به نظر قدیمی میرسد، legacy به نظر خواهد رسید.
