Уровень сложности
Простой
Время на прочтение
9 мин
Количество просмотров 6.1K
JSON — это инструмент, которым каждый веб-разработчик пользуется ежедневно. На первый взгляд кажется, что JSON слишком прост и незамысловат, чтобы читать про него целую статью. Но если вы не знаете, что такое санитизация данных, никогда не слышали про JSON Injection или XSS атаки, если информация о том, что JSON.parse и JSON.stringify принимают 2 и 3 аргумента для вас новость, то эта статья будет вам полезна.
Общие положения
JSON (JavaScript Object Notation) — это легковесный формат данных, который используется для обмена данными между клиентом и сервером в веб-приложениях.
В начале 2000-х годов была потребность в формате данных, который был бы удобнее и проще XML. Дуглас Крокфорд предложил использовать для обмена данными формат объектного литерала JavaScript, так и появился JSON. Предложенный Крокфордом формат оказался более компактным по сравнению с XML, проще для чтения и записи и прост в парсинге и генерации. В результате сейчас JSON является основным форматом обмена данными в веб-разработке.
JSON основан на двух основных структурах данных: объекте и массиве. Объект — это набор пар «ключ-значение», массив — это упорядоченный список значений. Ключи в объектах JSON всегда являются строками, а значения могут быть строками, числами, объектами, массивами, булевыми значениями или null.
{
"name": "John",
"age": 30,
"is_student": false,
"courses": ["math", "history", "chemistry"],
"personal_info": {
"address": "123 Main St",
"phone_number": "123-456-7890"
}
}
Несмотря на простоту и удобство, работа с JSON может быть связана с рядом проблем безопасности, если не подходить к этому процессу обдуманно.
Безопасное использование JSON
Основные виды атак
1. JSON Injection
Эта атака заключается в том, что злоумышленник вставляет вредоносный код в JSON, который затем обрабатывается веб-приложением.
Пример: Если веб-приложение берет пользовательский ввод и непосредственно вставляет его в JSON без проверки или санитизации (об этом подробно дальше), злоумышленник может вставить вредоносный код. Этот код может быть выполнен при обработке JSON.
const userInput = getUserInput();
const data = `{"name": "${userInput}"}`
const jsonData = JSON.parse(data);
В этом случае, если пользователь вводит что-то вроде "", "isAdmin": "true"}
, вредоносный код будет добавлен в JSON, создавая объект { "name": "", "isAdmin": "true"}
.
2. Cross-Site Scripting (XSS)
XSS — это тип атаки, при которой злоумышленник вставляет вредоносный скрипт в веб-страницу, который затем выполняется в браузере пользователя.
Пример: Если веб-приложение вставляет данные JSON напрямую в HTML без предварительной санитизации, это может привести к XSS-атаке.
const jsonData = getJsonData();
const element = document.getElementById('output');
element.innerHTML = jsonData.content;
Если jsonData.content
содержит что-то вроде
<script>maliciousCode()</script>
вредоносный код будет выполнен в браузере пользователя.
3. SQL Injection и JSON
SQL-инъекция — это вид атаки, при которой злоумышленник внедряет или «инъецирует» вредоносный SQL код в запрос к базе данных через ввод пользователя. SQL-инъекции могут привести к несанкционированному доступу к базе данных, краже, изменению или уничтожению данных.
Примером может служить ситуация, когда данные с клиентской стороны, например, из формы на веб-странице, принимаются в формате JSON, и затем используются для построения SQL-запроса.
const userData = JSON.parse(userInput);
const query = `SELECT * FROM users WHERE name = '${userData.name}' AND password = '${userData.password}'`;
Злоумышленник может подставить в поле name
или password
строку, которая, будучи вставлена в запрос SQL, изменит его логику. Например, следующий JSON:
{
"name": "' OR '1'='1'; --",
"password": "any"
}
после подстановки в запрос приведет к его изменению:
SELECT * FROM users WHERE name = '' OR '1'='1'; --' AND password = 'any'
В данном случае, поскольку условие '1'='1'
всегда истинно, это приведет к выбору всех пользователей и злоумышленник сможет обойти проверку пароля и войти в систему под любым аккаунтом.
Дисклеймер: я привел стандартный пример SQL-инъекции, он призван демонстрировать концепцию, а не реальный вектор атаки.
Важно понимать, что JSON сам по себе не имеет отношения к SQL-инъекциям. Однако, если данные, полученные в формате JSON, используются для формирования SQL-запросов без предварительной валидации и санитизации, то это может создать уязвимости для таких атак.
Чтобы предотвратить SQL-инъекции, рекомендуется использовать подготовленные или параметризованные запросы, а также проводить валидацию и санитизацию всех входных данных. Подготовленные запросы используют параметры вместо прямой вставки ввода пользователя в запрос. Библиотеки баз данных обычно предоставляют функции для создания подготовленных запросов.
Пример использования подготовленного запроса на Node.js с использованием MySQL:
const userData = JSON.parse(userInput);
let sql = `SELECT * FROM users WHERE name = ? AND password = ?`;
const inserts = [userData.name, userData.password];
sql = mysql.format(sql, inserts);
В этом примере, даже если злоумышленник попытается внедрить вредоносный код через JSON, он будет безопасно обработан библиотекой MySQL, и SQL-инъекция не произойдет.
Предотвращение атак
Основой безопасного использования JSON являются валидация и санитизация данных. Эти инструменты универсальны и эффективны против всех описанных выше видов атак.
Санитизация данных
Санитизация данных — это процесс очистки данных от вредоносных или нежелательных элементов. В контексте веб-разработки это обычно включает удаление или замену символов, которые могут быть использованы для проведения атак, таких как вставка SQL, XSS или JSON Injection.
Инструменты для санитизации данных:
-
DOMPurify: Это библиотека для JavaScript, которая позволяет очищать HTML, MathML и SVG от XSS атак. DOMPurify очень прост в использовании. Просто передайте ей строку, которую нужно санитизировать, и она вернет безопасную строку.
const clean = DOMPurify.sanitize(dirty);
-
express-validator: Это набор middleware функций для Express.js, который предоставляет мощные инструменты валидации и санитизации.
const express = require('express');
const { query, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
app.get('/hello', query('person').notEmpty().escape(), (req, res) => {
const result = validationResult(req);
if (result.isEmpty()) {
return res.send(`Hello, ${req.query.person}!`);
}
res.send({ errors: result.array() });
});
app.listen(3000);
В этом примере ‘express-validator’ проверяет, что имя пользователя не пустое, а вызов escape()
производит санитизацию данных.
Валидация данных
Валидация — это процесс проверки данных на соответствие определенным критериям или правилам. В контексте JSON валидация обычно включает в себя проверку следующего:
-
Соответствие структуры и типов данных предполагаемым. Например, если ожидается объект с определенными полями, валидация должна убедиться, что все эти поля присутствуют и имеют правильные типы данных.
-
Отсутствие нежелательных символов или паттернов, которые могут вызвать проблемы при обработке данных (например, вредоносный код).
-
Соответствие данных бизнес-правилам. Например, валидация может включать проверку того, что значение определенного поля находится в допустимом диапазоне.
JavaScript предоставляет некоторые базовые функции для валидации JSON.
JSON.stringify
JSON.stringify() преобразует объект JavaScript в строку JSON. Этот метод принимает три аргумента: значение для преобразования, функцию-заменитель и пробелы.
-
Значение: это может быть любой объект JavaScript или значение, которое вы хотите преобразовать в JSON.
-
Функция-заменитель (replacer): это опциональная функция, которая преобразует значения объекта перед их сериализацией. Она может использоваться для фильтрации или преобразования значений.
-
Пробелы: опциональный параметр, который контролирует отступы в сериализованном JSON. Это может быть число (количество пробелов) или строка (до 10 символов, используемых в качестве пробела).
const json = JSON.stringify(obj, (key, value) => {
if (key === 'id') return undefined;
return value;
}, 2);
В этом примере значение «id» удаляется из сериализованного JSON и используются два пробела для отступов в JSON.
Сериализация — это процесс преобразования состояния объекта в формат, который может быть сохранен или передан и затем восстановлен. В контексте JSON, сериализация обычно включает преобразование объекта JavaScript в строку JSON.
Десериализация — это обратный процесс. Он преобразует сериализованные данные обратно в их первоначальную форму. В контексте JSON, десериализация обычно включает преобразование строки JSON обратно в объект JavaScript.
JSON.parse
JSON.parse() преобразует строку JSON обратно в объект JavaScript. Этот метод принимает два аргумента: строку для преобразования и функцию-ревайвер.
-
Строка: это должна быть корректная строка JSON, которую вы хотите преобразовать в объект JavaScript.
-
Функция-ревайвер (reviver): это опциональная функция, которая преобразует значения объекта после их десериализации. Она может использоваться для преобразования или восстановления определенных значений.
const obj = JSON.parse(str, (key, value) => {
if (key === 'date') return new Date(value);
return value;
});
В примере выше, функция-ревайвер преобразует строку в объект Date в процессе десериализации JSON.
JSON Schema
Для более сложных случаев валидации, таких как проверка структуры объекта JSON и типов данных, может потребоваться использование сторонних библиотек, таких как Ajv (Another JSON Schema Validator).
Ajv использует стандарт, который называется JSON Schema который нужен для описания и валидации структуры JSON-данных.
Пример JSON Schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Product",
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"price": {
"type": "number",
"exclusiveMinimum": 0
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
}
},
"required": ["id", "name", "price"]
}
Обратите внимание на поле:
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema"
является особым полем в JSON Schema. Это URI, который указывает на версию JSON Schema, которая была использована для создания вашей схемы. Это необходимо для корректной интерпретации и валидации схемы.
Вышеуказанная схема описывает объект «Product» с полями «id», «name», «price» и «tags». Поля «id», «name» и «price» являются обязательными. Поле «tags» должно быть массивом уникальных строк.
В данном примере, "$schema": "http://json-schema.org/draft-07/schema#"
указывает, что ваша схема написана с использованием версии "draft-07"
JSON Schema. Это помогает программам, которые обрабатывают вашу схему, правильно интерпретировать и применять правила валидации.
Ajv позволяет определить схему для данных, а затем проверять объекты на соответствие этой схеме.
const Ajv = require("ajv")
const ajv = new Ajv()
const schema = {
type: "object",
properties: {
foo: {type: "integer"},
bar: {type: "string"}
},
required: ["foo","bar"],
additionalProperties: false
}
const data = {foo: 1, bar: "abc"}
const valid = ajv.validate(schema, data)
if (!valid) console.log(ajv.errors)
В этом примере, если данные не соответствуют схеме (например, foo
не является числом, или одно из полей отсутствует), Ajv вернет ошибки, которые затем можно обработать.
Общие ошибки при работе с JSON и их решения
Синтаксические ошибки, ошибки сериализации/десериализации и ошибки передачи данных — это некоторые из наиболее распространенных проблем, с которыми разработчики сталкиваются при работе с JSON.
-
Синтаксические ошибки: JSON имеет строгий синтаксис. Например, все ключи должны быть заключены в двойные кавычки, а не в одинарные.
{ 'key': "value" } // ошибка { "key": "value" } // правильно
Кроме того, каждый элемент в массиве или пара ключ-значение в объекте должны быть разделены запятыми.
{ "key1": "value1" "key2": "value2" } // ошибка { "key1": "value1", "key2": "value2" } // правильно
Синтаксические ошибки могут быть легко обнаружены с помощью JSON валидаторов, например, JSONLint.
-
Ошибки сериализации/десериализации: Некоторые объекты JavaScript не могут быть корректно сериализованы в JSON или десериализованы из JSON. Например, функции и объекты Date в JavaScript не могут быть преобразованы в JSON. В этих случаях необходимо преобразовать объекты в поддерживаемые типы данных перед сериализацией или после десериализации. В этом примере, при преобразовании JSON обратно в объект, функция ревайвер преобразует строку даты обратно в объект Date. Без функции ревайвер, дата осталась бы строкой.
const user = { name: 'Tom', registered: new Date(), }; const json = JSON.stringify(user); // Преобразуем JSON обратно в объект, восстанавливая дату с помощью функции ревайвер const parsedUser = JSON.parse(json, (key, value) => { if (key == 'registered') return new Date(value); return value; }); console.log(parsedUser.registered); // Выведет объект Date, а не строку
-
Ошибки при передаче данных: JSON часто используется для передачи данных между клиентом и сервером. Если эти данные не правильно кодируются или декодируются, или если они теряются в процессе передачи, это может привести к ошибкам. Обычно ошибки кодирования и декодирования связаны с ошибками в заголовке ‘Content-Type’, там необходимо указать значение ‘application/json’.
'Content-Type': 'text/plain' // ошибка
'Content-Type': 'application/json' // правильно
Заключение
Правильное использование JSON в веб-разработке критически важно для обеспечения безопасности и эффективности приложения. Валидация и санитизация данных, использование безопасных функций для обработки JSON и избегание распространенных ошибок могут помочь обеспечить надежную и безопасную работу с JSON. Надеюсь теперь вы согласитесь, что даже в таком простом инструменте, как JSON, могут быть неожиданные подводные камни и особенности, о которых нужно знать.
Также напоследок хочу пригласить вас на бесплатный урок, на котором поговорим о том, как работать с многопоточностью в NodeJS. Также на уроке вы познакомитесь с WorkerThreads. Регистрация доступна по ссылке.
Полезные ссылки
Что еще почитать по теме:
-
Документация MDN по JSON
-
Руководство OWASP по предотвращению SQL инъекций.
-
Подробнее о истории и важности JSON можно прочитать здесь и здесь.
-
Подробнее про JSON Schema тут и тут.
Инструменты:
-
Ajv (Another JSON Schema Validator): Один из самых популярных JSON Schema валидаторов. Он позволяет создавать сложные правила валидации, используя JSON Schema стандарт.
-
DOMPurify: Библиотека для санитизации HTML, которая помогает предотвращать XSS атаки.
-
express-validator: Библиотека для валидации и санитизации данных в Express.js приложениях.
-
Joi: Мощная библиотека для валидации данных в JavaScript. Она поддерживает множество типов данных и позволяет создавать сложные правила валидации.
-
validator.js: Библиотека строковой валидации и санитизации.
-
js-xss: Безопасный и мощный модуль санитизации HTML для JavaScript, предназначенный для предотвращения XSS атак.
-
sanitize-html: Библиотека, которая позволяет обрабатывать HTML, сохраняя только те элементы и атрибуты, которые вы хотите разрешить.
This page contains additional examples of how to apply various parts of the specification.
Sparse Fieldsets
Examples of how sparse fieldsets work.
Basic request:
GET /articles?include=author HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2015-05-22T14:56:29.000Z",
"updated": "2015-05-22T14:56:28.000Z"
},
"relationships": {
"author": {
"data": {"id": "42", "type": "people"}
}
}
}],
"included": [
{
"type": "people",
"id": "42",
"attributes": {
"name": "John",
"age": 80,
"gender": "male"
}
}
]
}
Request with fields[articles]
and fields[people]
parameters:
GET /articles?include=author&fields[articles]=title,body,author&fields[people]=name HTTP/1.1
Note: The above example URI shows unencoded
[
and]
characters simply
for readability. In practice, these characters should be percent-encoded, as
noted in the base specification. See “Square Brackets in Parameter Names”.
Here we want articles
objects to have fields title
, body
and author
only and people
objects to have name
field only.
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever."
},
"relationships": {
"author": {
"data": {"id": "42", "type": "people"}
}
}
}],
"included": [
{
"type": "people",
"id": "42",
"attributes": {
"name": "John"
}
}
]
}
Pay attention to the fact that you have to add a relationship name both in include
and fields
(since relationships are fields too), otherwise you’ll get:
GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever."
}
}],
"included": [
{
"type": "people",
"id": "42",
"attributes": {
"name": "John"
}
}
]
}
Note: The above example URI shows unencoded
[
and]
characters simply
for readability. In practice, these characters should be percent-encoded, as
noted in the base specification. See “Square Brackets in Parameter Names”.
Example of a page-based strategy on how to add pagination links.
Basic request:
GET /articles?page[number]=3&page[size]=1 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"meta": {
"totalPages": 13
},
"data": [
{
"type": "articles",
"id": "3",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2015-05-22T14:56:29.000Z",
"updated": "2015-05-22T14:56:28.000Z"
}
}
],
"links": {
"self": "http://example.com/articles?page[number]=3&page[size]=1",
"first": "http://example.com/articles?page[number]=1&page[size]=1",
"prev": "http://example.com/articles?page[number]=2&page[size]=1",
"next": "http://example.com/articles?page[number]=4&page[size]=1",
"last": "http://example.com/articles?page[number]=13&page[size]=1"
}
}
Note: The above example URIs show unencoded
[
and]
characters simply
for readability. In practice, these characters should be percent-encoded, as
noted in the base specification. See “Square Brackets in Parameter Names”.
Note: Putting a property like
"totalPages"
in"meta"
can be a convenient way
to indicate to clients the total number of pages in a collection (as opposed to
the"last"
link, which simply gives the URI of the last page). However, all
"meta"
values are implementation-specific, so you can call this member whatever
you like ("total"
,"count"
, etc.) or not use it at all.
Error Objects
Examples of how error objects work.
A Basic Error Object
In the response below, the server is indicating that it encountered an error
while creating/updating the resource, and that this error was caused
by an invalid "firstName"
attribute:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "422",
"source": { "pointer": "/data/attributes/firstName" },
"title": "Invalid Attribute",
"detail": "First name must contain at least two characters."
}
]
}
Every member in an error object is optional, but all help the client
by providing extra details.
The source
member is used to indicate
which part of the request document caused the error.
The title
and detail
members are similar, but detail
is specific
to this occurrence of the problem, whereas title
is more generic.
The status
member represents the HTTP status code associated with the problem.
It’s very helpful when multiple errors are returned at once (see below), as the
HTTP response itself can only have one status code. However, it can also be
useful for single errors, to save clients the trouble of consulting the HTTP
headers, or for using JSON:API over non-HTTP protocols, which may be officially
supported in the near future.
Multiple Errors
When multiple errors occur in response to a single request, the server
can simply add each error to the errors
array:
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "403",
"source": { "pointer": "/data/attributes/secretPowers" },
"detail": "Editing secret powers is not authorized on Sundays."
},
{
"status": "422",
"source": { "pointer": "/data/attributes/volume" },
"detail": "Volume does not, in fact, go to 11."
},
{
"status": "500",
"source": { "pointer": "/data/attributes/reputation" },
"title": "The backend responded with an error",
"detail": "Reputation service not responding after three requests."
}
]
}
The only uniqueness constraint on error objects is the id
field. Thus,
multiple errors on the same attribute can each be given their own error
object. The example below shows multiple errors on the "firstName"
attribute:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"source": { "pointer": "/data/attributes/firstName" },
"title": "Invalid Attribute",
"detail": "First name must contain at least two characters."
},
{
"source": { "pointer": "/data/attributes/firstName" },
"title": "Invalid Attribute",
"detail": "First name must contain an emoji."
}
]
}
Note: in the responses above with a 422 status code,
400 Bad Request
would
also be acceptable. (More details.)
JSON:API doesn’t take a position on 400 vs. 422.
Error Codes
The code
member of an error object contains an application-specific code
representing the type of problem encountered. code
is similar to title
in that both identify a general type of problem (unlike detail
, which is
specific to the particular instance of the problem), but dealing with code
is easier programatically, because the “same” title
may appear in different
forms due to localization.
For the example below, imagine the API docs specifed the following mapping:
Code | Problem |
---|---|
123 | Value too short |
225 | Password lacks a letter, number, or punctuation character |
226 | Passwords do not match |
227 | Password cannot be one of last five passwords |
Multiple errors on "password"
attribute, with error code
:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"jsonapi": { "version": "1.0" },
"errors": [
{
"code": "123",
"source": { "pointer": "/data/attributes/firstName" },
"title": "Value is too short",
"detail": "First name must contain at least two characters."
},
{
"code": "225",
"source": { "pointer": "/data/attributes/password" },
"title": "Passwords must contain a letter, number, and punctuation character.",
"detail": "The password provided is missing a punctuation character."
},
{
"code": "226",
"source": { "pointer": "/data/attributes/password" },
"title": "Password and password confirmation do not match."
}
]
}
Notice that this response includes not only the errors
top-level member,
but the jsonapi
top-level member. Error responses cannot contain the
top-level data
member, but can include all the other top-level members
JSON:API defines.
Also, notice that the third error object lacks a detail
member (perhaps
for security). Again, all error object members are optional.
Advanced source
Usage
In the example below, the user is sending an invalid JSON:API
request, because it’s missing the data
member:
PATCH /posts/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{ "datum": [ ] }
Therefore, the server responds:
HTTP/1.1 422 Unprocesssable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"source": { "pointer": "" },
"detail": "Missing `data` Member at document's top level."
}
]
}
It uses source
to point to the top-level of the document (""
).
(Pointing to “/” would be an appropriate reference to the string
"some value"
in the request document {"": "some value"}
.
Pointing to "/data"
would be invalid because the request document did
not have a value at "/data"
, and source
is always given with reference
to the request document.)
If the server cannot parse the request as valid JSON, including
source
doesn’t make sense (because there’s no JSON document for source
to
refer to). Here’s how the server might respond to an invalid JSON document:
{
"errors": [{
"status": "400",
"detail": "JSON parse error - Expecting property name at line 1 column 2 (char 1)."
}]
}
Invalid Query Parameters
The source
member can also be used to indicate that the error originated
from a problem with a URI query parameter, like so:
GET /api/posts/1?include=author HTTP/1.1
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"source": { "parameter": "include" },
"title": "Invalid Query Parameter",
"detail": "The resource does not have an `author` relationship path."
}
]
}
In most cases, JSON:API requires the server to return an error when it encounters
an invalid value for a JSON:API–defined query parameter. However, for API-specific
query parameters (i.e. those not defined by JSON:API), a server may choose to
ignore an invalid parameter and have the request succeed, rather than respond with
an error. API-specific query parameters must contain one non a-z
character.
Other examples of invalid parameters include: ?fields[people]=
(invalid parameter name;
should be fields[people]
) and ?redirect_to=http%3A%2F%2Fwww.owasp.org
(invalid parameter,
in this case, a phishing attack), etc.
JSON ( JavaScript Object Notation), is widely used format for asynchronous communication between webpage or mobile application to back-end servers. Due to increasing trend in Single Page Application or Mobile Application, popularity of the JSON is extreme.
Why do we get JSON parse error?
Parsing JSON is a very common task in JavaScript. JSON.parse() is a built-in method in JavaScript which is used to parse a JSON string and convert it into a JavaScript object. If the JSON string is invalid, it will throw a SyntaxError.
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json);
console.log(obj.count);
// expected output: 42
console.log(obj.result);
// expected output: true
How to handle JSON parse error?
There are many ways to handle JSON parse error. In this post, I will show you how to handle JSON parse error in JavaScript.
1. Using try-catch block
The most common way to handle JSON parse error is using try-catch block. If the JSON string is valid, it will return a JavaScript object. If the JSON string is invalid, it will throw a SyntaxError.
try {
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json);
console.log(obj.count);
// expected output: 42
console.log(obj.result);
// expected output: true
} catch (e) {
console.log(e);
// expected output: SyntaxError: Unexpected token o in JSON at position 1
}
2. Using if-else block
Another way to handle JSON parse error is using if-else block.
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json);
if (obj instanceof SyntaxError) {
console.log(obj);
// expected output: SyntaxError: Unexpected token o in JSON at position 1
} else {
console.log(obj.count);
// expected output: 42
console.log(obj.result);
// expected output: true
}
3. Using try-catch block with JSON.parse()
The third way to handle JSON parse error is using try-catch block with JSON.parse().
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json, (key, value) => {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
});
console.log(obj.count);
// expected output: 42
console.log(obj.result);
// expected output: true
4. Using try-catch block with JSON.parse() and JSON.stringify()
The fourth way to handle JSON parse error is using try-catch block with JSON.parse() and JSON.stringify(). If the JSON string is valid, it will return a JavaScript object. If the JSON string is invalid, it will return a SyntaxError.
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json, (key, value) => {
try {
return JSON.parse(value);
} catch (e) {
return value;
}
});
const str = JSON.stringify(obj);
console.log(str);
// expected output: {"result":true,"count":42}
All the world uses REST style web services, and returns data in JSON structures, because that is the best kind of API for an interactive, responsive JavaScript based user interface that runs in a browser. What are the best practices for what to send back when the server is unable to handle the request it was given?
A service call (also known as an API) is designed to accept some information (parameters), process them with some data store it might have, and then respond with output that meets a specified need.
Success is defined as when it is able and allowed to do this. If you ask for the average age of all customers who have visited in the last month, then success is when that is calculated correctly (according to your available data) and returned.
Failure is then anything that is not success. It could be something big, like the server crashing, or a power outage. Maybe a bug in the code causes a null pointer exception. Or it could be that the DB server that holds the customer data is not available at the moment. Or it could be that the parameter specifies a store that does not exist. Or the date range might be in the future and impossible to calculate. Or it might simply be that the user making the request is not allowed to make this call. Sometimes we think that a failure has to be a breakdown, but in this case it is not. Any reason that prevents the service from responding with the correct result (that the server knows about) will be a failure.
The response can be incorrect without being a failure. If someone manipulated the data so that all values were 10x what they should be, then the service will return an answer that is 10x from what it should be. Clearly there are incorrect results that the server can not detect. The server will do some tests (e.g. checking that the store exists) and if a test fails, then it will be a failure. If no test on the server shows any problem, then it will be considered a successful response.
For this discussion, error and exception will be considered equivalent. If the service is able to successfully handle the request and return the result, then there is no error, no exception. If it determines that there is a problem with the request, it will say an exception occurred, and that it needs to communicate the exception (the error) to the client.
Goals
- This is specifically for support of a JS user interface fetching and sending data. Clearly server to server might use something else, I don’t know.
- Need to fit the style where JS works asynchronously: you make a request and register a call-back function to received the response when it is ready.
- We want to fulfill the design goals written in “The Purpose of Error Reporting“. Specifically, we need to be able to give enough information to allow the user to resolve the problem if there is one.
- We need to consider that an error message is not a simple short statement, as written in “Gathering Error Report Information.” The system will often not know precisely how to instruct the user to resolve the situation, and so need to include detailed information along with contextual information which come from different parts of the system. In short, you will have multiple separate messages to be taken together.
- It must be relatively easy to construct the UI to respond to the error correctly.
- It must fit with the infrastructure that is supporting it. For example, if Apache web service is fronting the requests for the real server, when Apache can not understand the request, it will return a 404 error along with some HTML description.
Basic REST Paradigm
The HTTP protocol is intimately together with the API. It is not a matter of layering one on the other. So when you want to get some information, you use the GET method. When you want to upload some information, you use PUT or POST.
The open question is: use the response code or not. Option 1: always return 200 (everything is OK) and then in JSON envelope, have a success or error indicator. Option 2: return 200 only on success, and return a different code on failure. In either case, the returned data will need additional information about the problem, so clearly the return code is not sufficient by itself.
The advantage of using 200 for success, and some other value for failure, on top of the exception information is that many JS methods for making the call have different callback method for success and failure, and that depends on the return code. There are three reasons to use this: First, having an explicit place for a failure handler means it is harder to forget to handle the exception. Second, it separates the code that handles the well running cases, from the code that handled the exceptional cases. This is a good idea, since the purpose of those different codes is very different. The Third reason is that the data that the JS receives may be of a different format. If you are expecting JSON, and you get HTML, the next few statements that try to manipulate one as the other can cause confusing errors that point you in the wrong direction.
JSON in, JSON out
While it is possible to make a REST web service request in any format, and receive the response in any format, it is far better to stick with JSON in both ways if you have the choice. JSON is a well defined syntax; it faithfully represents all string data values without the XML limitation that all white space characters are equivalent; it is relatively compact; it is easily and rapidly parsed by any JS implementation; and it is supported in most other programming languages. XML brings a lot of problems with it, like namespace aliases, unclear semantics of the white space between nested tags, and unclear data types. You API will be far easier to use if you keep to JSON for both the request and the response.
What about exceptions? Your service may be designed to return a table of customer data. In the case of an exception, you might have no customer data, but instead information about the exception. Both may be in JSON, but their fields will be different. You have two choices:
- have the success response support one format, and the failure response a possibly different format
- make an “envelope” which has members for holding the response, and for holding the exception. On success the response field is filled in. On failure, the exception field is filled in.
This is a tough decision. The envelope design is clear and unambiguous, and the code for handling the exception will always find it in the same place in the envelope. But the envelope adds some overhead, and must be introduced at the time the web service is first defined. At least, adding an envelope later requires that all the clients of the API change. Some programmers (and by some I mean most) do not think about exceptions until after the API is functional for a few normal situations. Programmers don’t want to change at that time.
Returning a different JSON structure between success and failure is relatively easy to handle as long as the return code is an indicator of the format of the data. You should certainly only have one kind of error structure for each error code returned (otherwise how could you interpret it correctly?). So it seem reasonable to define each REST API call to have a structure that it returns on success (200) and another on failure (something other than 200).
What return code?
First we have to consider the limitations of the platform. We may define an API to be JSON in and JSON out, but there is no guarantee that is all the JS calling code will have to handle. For example, if the network is down, the browser will fail to connect, and you will get an error directly from the local environment, and it will not involve JSON of any structure. If the server is up, but mis-configured so that at the basic HTTP level it can not find the service, then you are likely to get a 404 error accompanied with HTML. If the web service is configured to only allow authorized access through, you might get a 403 and again no JSON.
When writing client code, it will be very convenient to have a response code that does not overlap with these so that you can test the response code and be relatively safe in assuming a particular structure. You can never be completely safe, since it all depends upon all the servers and proxy servers and firewalls that the request goes through. However, these typically only use a few.
Some suggest looking at the IANA List of Standard Status Codes and find the best match. In every case that I have examined closely, this would be a bad idea. By grouping errors into discrete categories, you are attempting to specify what the client should do in response. If you know that the client should do, then this is fine. But most errors appear to the programmer as “unexpected”. Because of this, it is likely that the API designer does not really know what the client should do to recover. Instead our goal is to display to the user the details of the problem. The standard numbers are based on the concept that an error is a “simple” thing: the disk is write protected and that the operation something really simple like “read a block of the disk”. Real services are very complicated and involve many components. You will never be able to categorize a failure according to a list of three digit integers.
In fact, if you find an error response that sounds close to what your error is, you are probably doing the client a disservice. For example, imagine the client calls your service, and you call another service, and that other service responds with 404 not found. Do you return 404 not found? I hope not because your service was found and handled the request just fine. Such code values might be OK for simple operations on direct connection, but most real world web services are far more than that.
So instead, pick an error that is “probably not” used by the network between the client and the server, and use that for ALL failures. For example 450. Responses to avoid are 400, 401, 403, 404, 500 and anything below 400 or above 599. If you detect a problem that the client might be able to automatically respond to, like (busy now, call back in 5 minutes) or (need another security token to access that resource) then you can add others, but these should be special cases that are carefully considered. The default position should be to return that one code unless there is a good reason.
What Exception Structure?
The most common and sensible structure is something like this:
{ "error": { "code": "400", "message": "main error message here", "target": "approx what the error came from", "details": [ { "code": "23-098a", "message": "Disk drive has frozen up again. It needs to be replaced", "target": "not sure what the target is" } ], "innererror": { "trace": [ ... ], "context": [ ... ] } } }
The response itself is an object. It has a member named “error” which is a well defined error structure. The ‘details’ is an array, each object in the array has a code and a message. There is an optional “innererror” that can hold a stack trace. This is structured defined by the OASIS OData standard. It seems to fit the need, and it is a standard, so let’s use that.
This structure can be used with the envelope approach where every response has the same top level. It also can be used as the structure only for error response. This member could be added into almost any object that does not already have an errors member.
If you are using 200 as the HTTP return code in all cases, then it will be necessary to have another field to contain whether it was success or not. I recommend a field named “success” which is true or false because this field could never mean anything else. A field called “status” could mean any number of things. If one is indicating error with the HTTP return code then you do not need this success field.
Real World Examples
Twitter: There is a page on their error codes and responses. I found amusement in the 420 error being defined as “Enhance Your Calm” when you exceed rate limits. Twitter defines a field “error” (which probably can be included in any regular response structure) which is an array of error objects, each object having a “code” and a “message”. They have a separate list of code values at this level, and the documentation matches code values with HTTP response codes (in most cases).
Stack Overflow: All responses are wrapped in a envelope. Error handling Page says they always return either 200 success, or 400 failure. There are special cases where other return codes are used. On a discussion forum someone said this: “Stack Overflow for example sends out an object with response, data and message properties. The response I believe contains true or false to indicate if the operation was successful (usually for write operations).”
The book Restful Web Services recommends using 400 for the error response in all cases.
Facebook, like Twitter, defines a “error” member, and that is an object with code, sub-code, type, message for developer, message for user, title for user. It looks like you could use this member in any return object, or by itself. They did not think about multiple errors. They seem to believe that an error code and a subcode will describe everything.
Amazon, defines error codes as strings, and maps them to HTTP response codes. Their response is XML, but it has a member “error” with the fields code, message, request id, and resource. They don’t seem to have a common envelope.
There is a proposal to extend HTTP to include more information, but we don’t need to wait for that. It is likely to be “error code” based.
The JSON API organization suggests to using a return code for failures, and suggests 400 might be suitable. They recommends a top level member “errors” which is an array of objects. The error object is very detailed, but includes: id (unique for this instance), links (to explanations of the error), status (the http return code), code (application specific string value), title, detail, and some other unlikely details.
JSend is another group trying to standardize this. They suggest always using an envelope. It has three members: status, data, message. Status can be “success”, “fail”, and “error”. A success response has status and data. Fail, strangely, has status and data, and the data field is where the error message is returned correlated with a field name going in. Error is, as you would expect, just status and message.
OASIS is working on an OData specification and they, like the others, define an error member which is single valued. The object there contains code (same as http reponse), message, target (the context for the request I think) , a subobject called “details” which is an array of (code, target, message), and then a member called “innererror” which contains a stack trace and context.
Hypertext Application Language (HAL) – There is a new July 2015 version of this. submitted to IETF, but there seems to be no discussion of errors in that.
References
I got material from the following places:
- REST API error return good practices – discussion on StackOverflow
- Stack Overflow
- Amazon
- IETF Draft Spec for error extension
- JSON API . org
- JSend
- OASIS