خطأ Unexpected End of JSON Input: معناه وطريقة حله

نفد النص أمام المحلّل قبل أن يجد قيمة كاملة، والسبب في أغلب الحالات أنه لم يكن هناك نص أصلًا.

تستدعي JSON.parse() أو response.json() فيظهر لك SyntaxError: Unexpected end of JSON input. وعلى خلاف خطأ الرمز غير المتوقع، لا تشير هذه الرسالة إلى محرف خاطئ، بل تعني أن المحلّل بلغ آخر محرف وهو لا يزال ينتظر قيمة. فإما أن النص كان فارغًا، وإما أنه توقف في منتصف المستند. التمييز بين هاتين الحالتين هو المهمة كلها، ويكفيه سطر واحد من التسجيل.

الخطأ نفسه في بيئات مختلفة

أين تراهالرسالة
Chrome وEdge وNode.js، JSON.parseUnexpected end of JSON input
Chrome وEdge، response.json()الرسالة نفسها، وأحيانًا تسبقها العبارة Failed to execute 'json' on 'Response':
Chrome، استجابة بلا جسم على الإطلاق، مثل الاستجابة المعتمة لطلب no-corsUnexpected end of input
FirefoxJSON.parse: unexpected end of data at line 1 column 1 of the JSON data
SafariJSON Parse error: Unexpected EOF
Python، json.loadsExpecting value: line 1 column 1 (char 0)

حين تذكر الرسالة السطر 1 والعمود 1 أو المحرف 0، فالمحلّل لم يجد شيئًا يقرؤه على الإطلاق. والنص الفارغ ليس JSON صالحًا: أقصر المستندات الصالحة قيم مفردة مثل 0 أو null أو "" أو {}.

السبب المعتاد: جسم فارغ أو نص فارغ

قبل أن تبحث عن خلل في البيانات، تحقّق أولًا من وجود بيانات أصلًا. هذه هي المصادر الشائعة للنص الفارغ:

  • 204 No Content. كثير من واجهات الـ API تردّ على طلب DELETE أو PUT ناجح بالحالة 204 ودون جسم. وقيمة res.ok هي true مع 204، لذا فإن فحصها لا يحمي استدعاء res.json() الذي يليه.
  • طلبات HEAD. الاستجابة لطلب HEAD لا تحمل جسمًا أبدًا، مهما قالت ترويساتها.
  • fetch مع mode: 'no-cors'. هذا الخيار لا يتجاوز قيود CORS، بل يعيد استجابة معتمة بالحالة 0 وبجسم لا يُسمح لكودك بقراءته، فيفشل تحليلها دائمًا. فعّل CORS على الخادم، أو استدعِ الـ API عبر الواجهة الخلفية الخاصة بك.
  • استجابة 200 أو 500 لا شيء فيها. معالج لا يكتب مخرجاته أبدًا، أو خطأ في الخادم ينهي الطلب قبل إرسال أي شيء، ينتجان جسمًا فارغًا برمز حالة عادي.
  • قيم تخزين وملفات فارغة. الاستدعاء localStorage.setItem('cart', '')، أو ملف settings.json فارغ، أو متغير بيئة مضبوط لكن قيمته فارغة، كلها تعطي JSON.parse نصًا فارغًا.
  • سطر جديد في نهاية JSON المفصول بالأسطر. الكود text.split('\n').map(JSON.parse) يفشل عند النص الفارغ الذي يلي آخر سطر جديد. استبعد الأسطر الفارغة أولًا.

وهناك حالتان متشابهتان في الظاهر لكنهما تتصرفان بشكل مختلف. المفتاح غير الموجود في localStorage يعيد null، وJSON.parse(null) يعيد null دون أن يرمي أي خطأ. أما المتغير غير المضبوط فقيمته undefined، وهذه تفشل برسالة مختلفة هي "undefined" is not valid JSON، وتجد شرحها في دليل Unexpected token in JSON.

لمعرفة الحالة التي أمامك، اقرأ الجسم كنص قبل تحليله:

const res = await fetch('/api/cart', { method: 'DELETE' });
const text = await res.text();
console.log(res.status, res.type, res.headers.get('content-type'), text.length);
// Typical results:
// 204 basic null 0  -> nothing to parse
// 0 opaque null 0   -> a no-cors request
// 200 basic application/json 8192 -> there is text: check whether it is complete

وتعرض لوحة Network في المتصفح المعلومة نفسها: لسان Response فارغ يعني أن المشكلة في الطلب أو في الخادم، لا في كود التحليل.

حين يكون النص مبتورًا

إن لم يكن النص فارغًا، فقد انتهى قبل أن يكتمل المستند. والإصدارات الحالية من Chrome وNode.js لا تقول "Unexpected end of JSON input" إلا حين يتوقف النص في مكان يُفترض أن تبدأ فيه قيمة، أو في منتصف true أو false أو null. وفي أي مكان آخر يُنتج البتر نفسه رسالة مختلفة:

ينتهي النص بـالرسالة في الإصدارات الحالية من Chrome وNode.js
{"id":Unexpected end of JSON input
{"tags":["a",Unexpected end of JSON input
{"ok":truUnexpected end of JSON input
{"name":"Al (11 محرفًا)Unterminated string in JSON at position 11
{"id":42 (8 محارف)Expected ',' or '}' after property value in JSON at position 8

القاعدة المفيدة هنا: إن كان الموضع المذكور في الرسالة يساوي طول النص، فالمستند مبتور وليس مشوّه البنية. وكانت الإصدارات الأقدم من Chrome وNode.js تصف معظم هذه الحالات أيضًا بعبارة "Unexpected end of JSON input"، ولهذا تستخدم السجلات والإجابات القديمة تلك الصياغة للبتر عمومًا.

يأتي JSON المبتور في العادة من أحد هذه المواضع:

  • تحليل تدفق قطعةً قطعة. البيانات القادمة من res.body.getReader() أو من حدث data في Node.js أو من مقبس شبكي تصل على أجزاء بأحجام اعتباطية، ونادرًا ما يكون الجزء الواحد مستندًا كاملًا. اجمع الأجزاء وحلّلها مرة واحدة في النهاية، أو احتفظ بـ JSON المفصول بالأسطر في مخزن مؤقت حتى يصل كل سطر جديد.
  • مساحة تخزين أصغر من اللازم. عمود في قاعدة بيانات أقصر من المستند، في قاعدة بيانات مضبوطة على البتر بدل رفض الكتابة، يخزّن الجزء الأول ويُسقط الباقي.
  • النسخ من مكان يختصر النصوص الطويلة، كنظام سجلات يفرض حدًا أقصى لطول السطر.
  • اتصال أُغلق مبكرًا أثناء استجابة ليس لها Content-Length ولم تُرسل مجزّأة (chunked). أما حين يكون الطول مُعلنًا، فيُبلَّغ عن انقطاع الاتصال كخطأ شبكة: في Node.js ترفض res.json() بالخطأ TypeError: terminated.

الحماية من الخطأ في الكود

اقرأ الجسم كنص، وقرّر ما الذي يعنيه الجسم الفارغ لهذه النقطة تحديدًا، ثم حلّل بعد ذلك فقط. بهذه الطريقة تعالج استجابات 204 وHEAD و200 الفارغة في مكان واحد، وتبقى رسالة الخطأ مفيدة حين يحدث خلل من نوع آخر:

async function readJson(res) {
  const text = await res.text();
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${text.slice(0, 100) || '(empty body)'}`);
  }
  if (text.trim() === '') return null;   // 204, HEAD or an empty 200
  try {
    return JSON.parse(text);
  } catch (err) {
    throw new Error(`Invalid JSON (${text.length} chars): ${err.message}`);
  }
}

أما القيم المخزّنة، فعامِل القيمة الفارغة والمفقودة بالطريقة نفسها وارجع إلى قيمة افتراضية:

function loadCart() {
  const raw = localStorage.getItem('cart');
  if (!raw) return [];          // null (missing) or '' (empty)
  try {
    return JSON.parse(raw);
  } catch {
    return [];                   // damaged value: start again
  }
}

وعلى جهة الخادم، التزم بنهج ثابت: إما أن تعيد 204 بلا جسم وبلا نوع محتوى JSON، وإما أن تعيد مستند JSON صغيرًا مثل {} أو {"deleted": true}. فاستجابة 200 موسومة بـ application/json وجسمها فارغ هي بالضبط التركيبة التي تكسر العملاء.

فحص النص يدويًا

حين لا يكون الجسم فارغًا، انسخه من لوحة Network أو من سجلاتك والصقه في مدقّق JSON. يستخدم المدقّق محلّل المتصفح نفسه، لذا يعرض الرسالة والموضع ذاتهما اللذين حصل عليهما كودك، وينسّق المستند متى أصبح صالحًا. وإن جاء الموضع المذكور في آخر ما لصقته تمامًا، فابحث عن الجزء المفقود في طبقة النقل أو التخزين لا في البيانات نفسها. أما الأخطاء التي تذكر محرفًا بعينه، مثل < القادم من صفحة HTML، فراجع لها دليل Unexpected token in JSON.

الأسئلة الشائعة

ماذا تعني رسالة "Unexpected end of JSON input"؟

بلغ المحلّل نهاية النص وهو لا يزال ينتظر قيمة. في الغالب كان النص فارغًا، كاستجابة 204 أو قيمة مخزّنة فارغة، وأحيانًا يكون المستند قد بُتر في منتصفه.

لماذا يفشل response.json() مع أن الطلب نجح؟

النجاح لا يعني وجود جسم. استجابة 204 No Content تكون فيها ok مساوية لـ true ولا جسم لها إطلاقًا. اقرأ الجسم بـ res.text() ولا تحلّله إلا إن لم يكن فارغًا.

هل يرمي JSON.parse(null) هذا الخطأ؟

لا. تتحوّل null إلى النص "null"، وهو JSON صالح، فيعيد JSON.parse(null) القيمة null. النص الفارغ هو ما يرمي "Unexpected end of JSON input"، أما undefined فيرمي خطأ مختلفًا.

كيف أميّز الجسم الفارغ من الجسم المبتور؟

سجّل طول النص قبل التحليل. الصفر يعني أنه فارغ. وإن كان الطول أكبر من الصفر وكان موضع الخطأ يساوي الطول، فالمستند مبتور.