الشبكات وHTTP

إرسال الطلبات ومعالجة الاستجابات

15 دقيقة الدرس 5 من 13

إرسال الطلبات ومعالجة الاستجابات

قدّمت الدرسُ السابق HttpClient وطريقة بنائه. الآن نضعه في العمل: نُنشئ طلبات GET وPOST، ونُرفق الترويسات، ونوفّر جسم الطلب، ونُفسّر HttpResponse الذي نتلقّاه بشكل صحيح. هذه هي الميكانيكيات اليومية لأي تكامل يعتمد على HTTP.

بناء HttpRequest

كل استدعاء HTTP يبدأ بـ HttpRequest يُبنى عبر واجهته البنّائة (fluent builder). الحد الأدنى الذي توفّره هو URI وأسلوب HTTP؛ كل شيء آخر اختياري لكنه كثيرًا ما يكون ضروريًا في الواقع العملي.

import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products/42")) .GET() // صريح وإن كان GET هو الافتراضي .header("Accept", "application/json") .header("X-Request-ID", "req-abc-123") .build();
HttpRequest كائن غير قابل للتعديل (Immutable). بمجرد بنائه لا يمكن تغييره، مما يعني أنه آمن للمشاركة بين الخيوط وإعادة استخدامه لمنطق إعادة المحاولة. لا تفترض أنك بحاجة لإعادة بناء كائن الطلب في كل محاولة — يمكنك إرسال نفس المثيل أكثر من مرة.

إرسال طلب GET

مرّر الطلب ومعالج الجسم (body handler) إلى client.send(). يُخبر معالج الجسم العميلَ كيف يحوّل البايتات الخام للاستجابة إلى النوع الذي تريده:

HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() // فك ترميز البايتات كـ String بترميز UTF-8 ); System.out.println("Status : " + response.statusCode()); System.out.println("Body : " + response.body());

تشمل معالجات الجسم المدمجة الشائعة: ofString() وofBytes() وofFile(Path) وofInputStream() وdiscarding() (للاستجابات التي لا يهمّنا جسمها). اختر الأنسب لما ستفعله بالبيانات — بث تنزيل ضخم إلى ملف بـ ofFile() يتجنّب تحميل جيجابايتات في ذاكرة الكومة.

قراءة بيانات وصف الاستجابة

تكشف HttpResponse<T> أكثر من مجرد الجسم:

int status = response.statusCode(); // 200 أو 404 أو 500 ... String body = response.body(); HttpHeaders headers = response.headers(); // الحصول على قيمة ترويسة واحدة (الاسم بأحرف صغيرة وفق مواصفة HTTP/2) String contentType = headers.firstValue("content-type").orElse("unknown"); // الحصول على كل قيم ترويسة (مثل Set-Cookie قد تظهر أكثر من مرة) java.util.List<String> cookies = headers.allValues("set-cookie"); // الـ URI بعد أي إعادة توجيه URI finalUri = response.uri(); // الإصدار الذي جرى التفاوض عليه (HTTP_1_1 أو HTTP_2) System.out.println(response.version());
تحقّق دائمًا من كود الحالة قبل الوثوق بالجسم. الاستجابات 4xx أو 5xx تُعيد في الغالب حمولة خطأ لا البيانات التي تتوقّعها. اكتب مساعدًا صغيرًا يرفع استثناءً ذا معنى لأكواد الحالة غير الـ 2xx حتى لا يعالج المستدعون JSON الخطأ كما لو كان نجاحًا.

إرسال طلب POST مع جسم JSON

تُوفّر طلبات POST ناشرَ جسم (body publisher) يُسلسل حمولتك إلى بايتات يمكن للعميل بثّها إلى الخادم. الحالة الأكثر شيوعًا هي نص JSON:

String json = """ { "name": "Wireless Keyboard", "price": 49.99, "stock": 200 } """; HttpRequest postRequest = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products")) .header("Content-Type", "application/json") .header("Accept", "application/json") .header("Authorization", "Bearer eyJhbGciOiJSUzI1NiJ9...") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> postResponse = client.send( postRequest, HttpResponse.BodyHandlers.ofString() ); if (postResponse.statusCode() == 201) { System.out.println("Resource created: " + postResponse.body()); } else { System.err.println("Unexpected status: " + postResponse.statusCode()); System.err.println("Error body: " + postResponse.body()); }

لطلب POST بدون جسم (نادر لكن صالح، مثل تشغيل نقطة نهاية action)، استخدم HttpRequest.BodyPublishers.noBody().

ناشرات الجسم المدمجة الأخرى

  • BodyPublishers.ofString(String) — نص مُرمَّز بـ UTF-8؛ العمل اليومي لـ JSON.
  • BodyPublishers.ofByteArray(byte[]) — بايتات خام؛ مفيد للبروتوكولات الثنائية أو البيانات المُسلسلة مسبقًا.
  • BodyPublishers.ofFile(Path) — يبثّ ملفًا مباشرة من القرص دون تحميله في الذاكرة؛ مثالي لنقاط نهاية رفع الملفات.
  • BodyPublishers.ofInputStream(Supplier<InputStream>) — يوفّر مجرى إدخال بشكل كسول؛ يدعم الحمولات الضخمة أو المُولَّدة ديناميكيًا.
  • BodyPublishers.noBody() — يُشير إلى جسم فارغ صراحةً (Content-Length: 0).

التعامل مع أكواد حالة HTTP باحترافية

يُعرّف HTTP أكواد الحالة في خمس فئات. فهمُ الفئات — لا مجرد حفظ الأرقام المفردة — هو ما يُمكّنك من كتابة معالجة أخطاء متينة:

  • 1xx إعلامية — إشارات على مستوى البروتوكول (مثل 100 Continue). يتعامل معها العميل تلقائيًا؛ نادرًا ما تراها.
  • 2xx نجاح200 OK (GET/PUT)، 201 Created (POST)، 204 No Content (DELETE بدون جسم).
  • 3xx إعادة توجيه — يتبع HttpClient إعادات التوجيه تلقائيًا عند ضبطه بـ followRedirects(HttpClient.Redirect.NORMAL).
  • 4xx خطأ العميل — الطلب خاطئ. 400 Bad Request، 401 Unauthorized، 403 Forbidden، 404 Not Found، 409 Conflict، 429 Too Many Requests.
  • 5xx خطأ الخادم — الخادم فشل. 500 Internal Server Error، 502 Bad Gateway، 503 Service Unavailable.

مساعد موجز يُرسّخ هذا المنطق:

private static <T> T requireSuccess(HttpResponse<T> response) { int status = response.statusCode(); if (status >= 200 && status < 300) { return response.body(); } throw new RuntimeException( "HTTP %d from %s".formatted(status, response.uri()) ); }
204 No Content يعني أن الجسم فارغ بموجب العقد. إن استخدمت BodyHandlers.ofString() لطلب DELETE يُعيد 204، فإن response.body() ستكون سلسلة فارغة — لا null. تمرير سلسلة فارغة لمحلل JSON سيرفع استثناءً. تحقّق من كود الحالة قبل إلغاء التسلسل، أو استخدم BodyHandlers.discarding() حين تعلم أن الجسم سيكون فارغًا.

ضبط الترويسات بشكل صحيح

تقع الترويسات في عدة فئات تستحق المعرفة الصريحة:

  • Content-Type — يصف الجسم الذي ترسله. اضبطه دائمًا في POST/PUT/PATCH. أشيع القيم application/json وapplication/x-www-form-urlencoded.
  • Accept — يُخبر الخادم بالصيغة التي يمكنك تحليلها. توفير application/json صريح ويتجنّب المفاجآت مع واجهات برمجية قد تُعيد XML أو HTML.
  • Authorization — تحمل بيانات الاعتماد. استخدم Bearer <token> لـ OAuth 2.0 / JWT، أو Basic <base64> لـ HTTP Basic auth. لا تمرّر بيانات الاعتماد كمعاملات استعلام قط.
  • User-Agent — يُعرّف عميلك. بعض الواجهات البرمجية تحدّ المعدل أو تحجب الطلبات التي لا تحمل سلسلة user agent ذات معنى.
  • Idempotency-Key (مثل Stripe) — UUID فريد تولّده لكل عملية منطقية حتى يتمكّن الخادم من إلغاء تكرار طلبات POST المُعاد محاولتها بأمان.
// ضبط ترويسات متعددة بشكل نظيف HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.stripe.com/v1/charges")) .header("Authorization", "Bearer sk_test_...") .header("Content-Type", "application/x-www-form-urlencoded") .header("Idempotency-Key", java.util.UUID.randomUUID().toString()) .header("User-Agent", "MyApp/2.1 (Java/" + System.getProperty("java.version") + ")") .POST(HttpRequest.BodyPublishers.ofString("amount=2000&currency=usd&source=tok_visa")) .build();

الخلاصة

أنت الآن تعرف كيف تصنع طلبات GET وPOST، وتُرفق ترويسات تعسّفية، وتختار ناشر الجسم المناسب لحمولتك، وتقرأ كود الحالة والترويسات والجسم من الاستجابة. هذه هي اللبنات التي يتألّف منها كل تفاعل HTTP في Java — أنماط العمل غير المتزامن وعملاء REST في الدروس القادمة تبني مباشرة على هذا الأساس.