خطای Unexpected Token در JSON یعنی چه و چطور رفعش کنیم؟
این خطا یعنی متنی که میخواستید پارس کنید اصلاً JSON نیست؛ توکنی که در پیام آمده معمولاً دقیقاً میگوید بهجای JSON چه چیزی دریافت کردهاید.
JSON.parse() یا response.json() را صدا میزنید و بهجای داده، یک SyntaxError تحویل میگیرید. متن پیام از مرورگری به مرورگر دیگر فرق میکند، اما معنایش همیشه یکی است: پارسر متن را از ابتدا خوانده و به نویسهای رسیده که در یک JSON معتبر نمیتواند در آن نقطه بیاید. همان نویسهای که در پیام نقل شده و موقعیتی که گزارش شده، مفیدترین سرنخهایی هستند که برای پیدا کردن علت در اختیار دارید؛ پس پیش از دست زدن به کد، پیام را با دقت بخوانید.
یک خطا با پیامهای متفاوت در هر موتور
نسخههای جدیدتر کروم و Node.js متن این پیام را عوض کردهاند، برای همین در نتایج جستجو و در لاگهای قدیمیتر با هر دو شکل آن روبهرو میشوید. جدول زیر نشان میدهد همین خطا در هر محیط چطور نوشته میشود:
| کجا میبینید | پیام |
|---|---|
| کروم، اج و Node.js 20 به بعد | Unexpected token '<', "<!DOCTYPE "... is not valid JSON |
| نسخههای قدیمیتر کروم و Node.js 18 | Unexpected token < in JSON at position 0 |
| فایرفاکس | JSON.parse: unexpected character at line 1 column 1 of the JSON data |
| سافاری | JSON Parse error: Unrecognized token '<' |
عبارت «position 0» یا «line 1 column 1» یعنی همان نویسه اول متن هم نادرست بوده است. این تقریباً همیشه نشان میدهد که پاسخ از اساس چیزی غیر از JSON بوده، نه اینکه داخل یک سند JSON اشتباه تایپی کوچکی رخ داده باشد. به بیان دیگر، در این حالت دنبال کامای جاافتاده نگردید؛ اول ببینید اصلاً چه چیزی به دستتان رسیده است.
علت معمول: صفحه HTML بهجای JSON
نویسه غیرمنتظره < در موقعیت صفر، با فاصله زیاد رایجترین شکل این خطاست. کد شما از یک API داده JSON خواسته، اما سرور در جواب یک صفحه HTML فرستاده است و HTML با <!DOCTYPE html> یا <html> شروع میشود. دلیلهای معمول این اتفاق:
- نشانی اشتباه: یک غلط تایپی یا جا افتادن پیشوند
/apiباعث میشود صفحه ۴۰۴ سایت برگردد. - خطای سرور: فریمورک بهجای داده، یک صفحه خطای ۵۰۰ به شکل HTML میسازد.
- نشست منقضیشده: درخواست به صفحه ورود هدایت میشود و
fetchبیسروصدا این ریدایرکت را دنبال میکند. - مسیر پیشفرض اپلیکیشن تکصفحهای (SPA): سرور توسعه یا هاست برای هر مسیر ناشناخته،
index.htmlرا با کد وضعیت ۲۰۰ برمیگرداند. - پراکسی، CDN یا فایروال که میان راه قرار دارد و صفحه خطا یا صفحه چالش HTML خودش را میفرستد.
سریعترین راه برای مطمئن شدن این است که پیش از پارس کردن، پاسخ خام را ببینید:
const res = await fetch('/api/items');
console.log(res.status, res.headers.get('content-type'));
const text = await res.text();
console.log(text.slice(0, 200)); // shows the HTML if that is what came back
در مرورگر هم میتوانید پنل Network را در ابزار توسعهدهنده باز کنید، روی همان درخواست بزنید و زبانه Response را ببینید. اگر آنجا HTML میبینید، مشکل را در نشانی درخواست، احراز هویت یا خطای سرور برطرف کنید؛ خود کدی که پارس میکند ایرادی ندارد و تغییر دادنش کمکی نمیکند.
توکنهای دیگر و اینکه به چه اشاره میکنند
همه پیامها به HTML ختم نمیشوند. نویسهای که در پیام آمده، یا عبارتی که پیش از «is not valid JSON» نقل شده، معمولاً مستقیم به علت اشاره میکند:
| پیام فعلی (پیام قدیمیتر) | چیزی که پارس کردهاید | راهحل |
|---|---|---|
"undefined" is not valid JSON (Unexpected token u in JSON at position 0) | مقدار undefined؛ مثلاً کلیدی که در localStorage وجود ندارد یا متغیری که مقدار نگرفته است | پیش از پارس کردن، مطمئن شوید مقدار وجود دارد |
"[object Object]" is not valid JSON (Unexpected token o in JSON at position 1) | شیئی که قبلاً پارس شده و JSON.parse آن را به رشته تبدیل کرده است | مستقیم از همان شیء استفاده کنید و دوباره پارسش نکنید |
Expected property name or '}' in JSON at position 1 | تککوتیشن یا کلید بدون کوتیشن، مثل {'a': 1} یا {a: 1} | همه کلیدها و رشتهها را داخل دابلکوتیشن بنویسید |
Expected double-quoted property name in JSON at position 8 | کامای انتهایی، مثل {"a": 1,} | کامای پیش از آکولاد بسته را حذف کنید |
Unexpected token 'N', "{"a": NaN}" is not valid JSON | NaN، Infinity یا undefined داخل داده | آنها را با null یا یک عدد جایگزین کنید |
| پیامی که در موقعیت صفر به توکنی نامرئی یا بهظاهر خالی اشاره میکند | نشانه ترتیب بایت (BOM) در ابتدای فایلی که با برخی ویرایشگرهای ویندوزی ذخیره شده است | فایل را با UTF-8 بدون BOM ذخیره کنید یا نویسه اول را حذف کنید |
پارس کردن دادهای که از قبل پارس شده
کتابخانهها اغلب شیء آماده تحویلتان میدهند. response.json() خودش یک شیء جاوا اسکریپت برمیگرداند و axios هم همین کار را در response.data انجام میدهد. اگر این شیء را دوباره به JSON.parse بدهید، اول به رشته "[object Object]" تبدیل میشود و پارس در موقعیت ۱ شکست میخورد، چون بعد از براکت اول نویسه o آمده است. راهحل فقط حذف همان پارس اضافه است:
const data = await res.json();
// JSON.parse(data) <- fails: data is already an object
console.log(data.items);
پیدا کردن اشتباه داخل یک JSON واقعی
وقتی موقعیت خطا جایی در میانه متن است، متن واقعاً JSON است و فقط اشتباهی درونش وجود دارد. متن را در اعتبارسنج JSON بچسبانید: این ابزار از همان پارسر مرورگر شما استفاده میکند، پیام خطا را همراه با موقعیت و شماره خط نشان میدهد و بهمحض اینکه سند معتبر شد، آن را مرتب و خوانا میکند. اشتباه اصلی معمولاً درست پیش از موقعیت گزارششده است؛ مثلاً کامایی که میان دو عضو جا افتاده یا کوتیشنی که باید دابلکوتیشن میبود.
پیشگیری از این خطا در کد
اگر پیش از پارس کردن، کد وضعیت و نوع محتوای پاسخ را بررسی کنید، یک خطای نحوی گیجکننده به پیامی تبدیل میشود که صریحاً میگوید چه اتفاقی افتاده است. تابع زیر همین کار را انجام میدهد و در پیام خطا، کد وضعیت، نوع محتوا و ابتدای بدنه پاسخ را هم میآورد:
async function getJson(url) {
const res = await fetch(url);
const type = res.headers.get('content-type') || '';
if (!res.ok || !type.includes('application/json')) {
const body = await res.text();
throw new Error(`Expected JSON from ${url}, got ${res.status} ${type}: ${body.slice(0, 100)}`);
}
return res.json();
}
همین فکر در سمت سرور هم به کار میآید: مسیرهای API باید بدنه خطا را به شکل JSON برگردانند، نه صفحه خطای HTML، و همراه آن هدر Content-Type: application/json را بفرستند. به این ترتیب کلاینت حتی هنگام خطا هم چیزی دریافت میکند که میتواند بخواند. برای پیام نزدیک به این خطا که وقتی بدنه پاسخ خالی یا نیمهکاره است ظاهر میشود، راهنمای خطای Unexpected end of JSON input را ببینید.