خطای Unexpected End of JSON Input یعنی چه و چطور رفعش کنیم؟

پارسر پیش از رسیدن به یک مقدار کامل به انتهای متن رسیده است؛ و در بیشتر موارد دلیلش این است که اصلاً متنی وجود نداشته.

همین حالا امتحان کنید: اعتبارسنجی JSON فرمت‌کننده JSON

JSON.parse() یا response.json() را صدا می‌زنید و با SyntaxError: Unexpected end of JSON input روبه‌رو می‌شوید. برخلاف خطای unexpected token، این خطا به نویسه اشتباهی اشاره نمی‌کند. معنایش این است که پارسر به آخرین نویسه رسیده، در حالی که هنوز منتظر یک مقدار بوده است. پس دو حالت بیشتر وجود ندارد: یا متن خالی بوده، یا وسط یک سند قطع شده است. تمام کار رفع خطا تشخیص همین دو حالت از یکدیگر است و برای آن یک خط لاگ کافی است.

یک خطا در محیط‌های مختلف

کجا می‌بینیدپیام
کروم، اج و Node.js، تابع JSON.parseUnexpected end of JSON input
کروم و اج، متد response.json()همان پیام، گاهی با پیشوند Failed to execute 'json' on 'Response':
کروم، پاسخی که هیچ بدنه‌ای ندارد، مثل پاسخ مات (opaque) در حالت no-corsUnexpected end of input
فایرفاکسJSON.parse: unexpected end of data at line 1 column 1 of the JSON data
سافاریJSON Parse error: Unexpected EOF
پایتون، تابع json.loadsExpecting value: line 1 column 1 (char 0)

وقتی پیام از خط ۱، ستون ۱ یا نویسه ۰ حرف می‌زند، یعنی پارسر اصلاً چیزی برای خواندن پیدا نکرده است. متن خالی JSON معتبر نیست: کوتاه‌ترین سندهای معتبر، مقدارهایی مثل 0، null، "" یا {} هستند.

علت معمول: بدنه خالی یا رشته خالی

پیش از اینکه دنبال ایرادی در داده بگردید، بررسی کنید اصلاً داده‌ای وجود داشته است یا نه. این‌ها منابع رایج رشته خالی‌اند:

  • پاسخ ۲۰۴ (No Content). بسیاری از APIها به یک DELETE یا PUT موفق، با کد وضعیت ۲۰۴ و بدون بدنه جواب می‌دهند. res.ok برای ۲۰۴ برابر true است، پس بررسی آن از فراخوانی res.json() که بعدش می‌آید محافظت نمی‌کند.
  • درخواست‌های HEAD. پاسخ به HEAD هیچ‌وقت بدنه ندارد، هدرهایش هرچه بگویند.
  • fetch با mode: 'no-cors'. این گزینه محدودیت CORS را دور نمی‌زند. نتیجه‌اش یک پاسخ مات (opaque) با کد وضعیت ۰ و بدنه‌ای است که کد شما اجازه خواندنش را ندارد، پس پارس کردنش همیشه شکست می‌خورد. CORS را روی سرور فعال کنید یا API را از طریق بک‌اند خودتان صدا بزنید.
  • پاسخ ۲۰۰ یا ۵۰۰ بدون هیچ محتوا. هندلری که هیچ‌وقت خروجی‌اش را نمی‌نویسد، یا خطای سروری که درخواست را پیش از ارسال هر چیزی تمام می‌کند، بدنه‌ای خالی با یک کد وضعیت معمولی تولید می‌کند.
  • مقدارهای ذخیره‌شده و فایل‌های خالی. 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 در 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 خالی است، مشکل در درخواست یا سرور است، نه در کدی که پارس می‌کند.

وقتی متن نیمه‌کاره قطع شده است

اگر متن خالی نیست، یعنی پیش از کامل شدن سند تمام شده است. کروم و Node.js فعلی فقط وقتی پیام «Unexpected end of JSON input» را نشان می‌دهند که متن درست جایی قطع شود که باید یک مقدار شروع شود، یا وسط true، false یا null. در هر جای دیگر، همین قطع شدن پیام دیگری تولید می‌کند:

انتهای متنپیام در کروم و Node.js فعلی
{"id":Unexpected end of JSON input
{"tags":["a",Unexpected end of JSON input
{"ok":truUnexpected end of JSON input
{"name":"Al (۱۱ نویسه)Unterminated string in JSON at position 11
{"id":42 (۸ نویسه)Expected ',' or '}' after property value in JSON at position 8

قاعده کاربردی این است: اگر موقعیتی که در پیام آمده با طول متن برابر باشد، سند قطع شده است، نه اینکه ساختار نادرستی داشته باشد. نسخه‌های قدیمی‌تر کروم و Node.js بیشتر این حالت‌ها را هم با «Unexpected end of JSON input» گزارش می‌کردند، برای همین در لاگ‌ها و پاسخ‌های قدیمی‌تر این عبارت برای هر نوع قطع شدن متن به کار رفته است.

JSON قطع‌شده معمولاً از یکی از این جاها می‌آید:

  • پارس کردن جریان داده تکه‌به‌تکه. داده‌ای که از res.body.getReader()، رویداد data در Node.js یا یک سوکت می‌رسد، در تکه‌هایی با اندازه دلخواه می‌آید و یک تکه تنها به‌ندرت سندی کامل است. تکه‌ها را جمع کنید و یک بار در انتها پارس کنید، یا در JSON خط‌به‌خط، داده را تا رسیدن هر خط جدید در بافر نگه دارید.
  • فضای ذخیره‌سازی کوچک‌تر از داده. ستونی از پایگاه داده که از سند کوتاه‌تر است، در پایگاه داده‌ای که تنظیم شده به‌جای رد کردن نوشتن، داده را کوتاه کند، بخش اول را ذخیره می‌کند و بقیه را دور می‌ریزد.
  • کپی کردن از جایی که متن طولانی را کوتاه می‌کند، مثل سامانه لاگی که برای طول هر خط سقف دارد.
  • اتصالی که زودتر از موعد بسته شده، روی پاسخی که Content-Length نداشته و به شکل chunked هم ارسال نشده است. وقتی طول اعلام شده باشد، قطع شدن اتصال به‌جای این خطا به شکل خطای شبکه گزارش می‌شود: در Node.js، res.json() با TypeError: terminated رد می‌شود.

پیشگیری از این خطا در کد

بدنه را به شکل متن بخوانید، تصمیم بگیرید بدنه خالی برای این endpoint چه معنایی دارد و فقط بعد از آن پارس کنید. این روش پاسخ‌های ۲۰۴، HEAD و ۲۰۰ خالی را در یک جا مدیریت می‌کند و وقتی مشکل دیگری پیش بیاید، پیام خطا را همچنان گویا نگه می‌دارد:

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
  }
}

در سمت سرور هم یکپارچه عمل کنید: یا ۲۰۴ را بدون بدنه و بدون نوع محتوای JSON برگردانید، یا یک سند JSON کوچک مثل {} یا {"deleted": true}. پاسخ ۲۰۰ با برچسب application/json و بدنه خالی، دقیقاً همان ترکیبی است که کلاینت‌ها را از کار می‌اندازد.

بررسی دستی متن

وقتی بدنه خالی نیست، آن را از پنل Network یا لاگ‌هایتان کپی کنید و در اعتبارسنج JSON بچسبانید. این ابزار از پارسر خود مرورگر استفاده می‌کند، پس همان پیام و موقعیتی را نشان می‌دهد که کد شما گرفته است و وقتی سند معتبر باشد، آن را فرمت می‌کند. اگر موقعیت گزارش‌شده درست در انتهای متنی است که چسبانده‌اید، بخش گمشده را در مسیر انتقال یا ذخیره‌سازی جستجو کنید، نه در خود داده. برای خطاهایی که نویسه مشخصی را نام می‌برند، مثل < از یک صفحه HTML، راهنمای خطای Unexpected token در JSON را ببینید.

سوالات متداول

Unexpected end of JSON input یعنی چه؟

پارسر به انتهای متن رسیده، در حالی که هنوز منتظر یک مقدار بوده است. معمولاً متن خالی بوده، مثلاً پاسخ ۲۰۴ یا مقدار ذخیره‌شده خالی. گاهی هم سند وسط راه قطع شده است.

چرا response.json() با وجود موفق بودن درخواست خطا می‌دهد؟

موفق بودن درخواست به معنای داشتن بدنه نیست. پاسخ ۲۰۴ No Content مقدار ok را true دارد و اصلاً بدنه ندارد. بدنه را با res.text() بخوانید و فقط وقتی خالی نیست پارسش کنید.

آیا JSON.parse(null) این خطا را می‌دهد؟

خیر. null به متن «null» تبدیل می‌شود که JSON معتبر است، پس JSON.parse(null) مقدار null برمی‌گرداند. رشته خالی خطای Unexpected end of JSON input می‌دهد و undefined خطای دیگری.

چطور بدنه خالی را از بدنه قطع‌شده تشخیص دهم؟

پیش از پارس، طول متن را لاگ کنید. صفر یعنی خالی. اگر طول بیشتر از صفر است و موقعیت خطا با طول برابر است، سند قطع شده است.
همین حالا امتحان کنید: اعتبارسنجی JSON فرمت‌کننده JSON