Когда использовать код состояния HTTP 404 в API


58

Я работаю над проектом и после споров с людьми на работе более часа. Я решил узнать, что могут сказать люди на стек-обмене.

Мы пишем API для системы, есть запрос, который должен возвращать дерево организации или дерево целей.

Дерево организации - это организация, в которой присутствует пользователь. Другими словами, это дерево должно существовать всегда. В организации дерево цели должно присутствовать всегда. (вот где начался спор). В случае, когда дерево не существует, мой коллега решил, что было бы правильно ответить на ответ с кодом состояния 200. И затем начал просить меня исправить мой код, потому что приложение разваливалось, когда нет дерева.

Я постараюсь пощадить пламя и ярость.

Я предложил поднять ошибку 404, когда нет дерева. По крайней мере, это дало бы мне знать, что что-то не так. При использовании 200 я должен добавить специальную проверку к моему ответу в обратном вызове успеха, чтобы обработать ошибки. Я ожидаю получить объект, но на самом деле я могу получить пустой ответ, потому что ничего не найдено. Звучит совершенно справедливо, если мы отмечаем ответ 404. И затем началась война, и я получил сообщение, что не понимаю схему кода состояния HTTP. Вот я и спрашиваю, что не так с 404 в этом случае? Я даже получил аргумент «Ничего не нашел , поэтому правильно вернуть 200». Я считаю, что это неправильно, поскольку дерево должно присутствовать всегда. Если мы ничего не нашли и ожидаем чего-то, это должен быть 404.

Больше информации,

Я забыл добавить выбранные URL-адреса.

организации

/OrgTree/Get

цели

/GoalTree/GetByDate?versionDate=...
/GoalTree/GetById?versionId=...

Моя ошибка, оба параметра обязательны. Если предоставляется какая-либо версия versionDate, которую можно проанализировать на определенную дату, она вернет закрытую ревизию. Если вы введете что-то в прошлом, он вернет первую ревизию. Если по идентификатору с идентификатором, который не существует, я подозреваю, что он вернет пустой ответ с 200.

дополнительный

Кроме того, я считаю, что лучшим ответом на проблему является создание объектов по умолчанию при создании организаций, поскольку отсутствие дерева не должно быть допустимым случаем и должно рассматриваться как неопределенное поведение. Невозможно использовать учетную запись без обоих деревьев. По этим причинам они должны присутствовать всегда.

также я связал это (один подобный, но я не могу найти это)

http://viswaug.files.wordpress.com/2008/11/http-headers-status1.png


Пожалуйста, уточните: как приложение может развалиться, когда нет дерева, если всегда есть дерево по предварительному условию ? (Я согласен с вами, это выглядит как 404)
Андрес Ф.

Ну, код не проверял нулевое значение, а анализировал строку json и как объект. В некотором месте в коде загруженный объект не присутствует, потому что он не может быть найден внутри.
Лоик Фор-Лакруа,

4
Было бы понятнее, если бы вы указали URI для ресурса, к которому вы пытаетесь получить доступ. Если это / цели / вы возвращаете 200 и пустой набор. Если вы пытаетесь получить доступ к / goal / {goal_id}, вы возвращаете 404. Если вы вернули 404 для запроса на / goal /, это означает, что URI не существует и больше не должен использоваться.
imel96

1
Тем не менее в обоих случаях вопрос остается в силе. Что должно /GoalTree/GetById?versionId=CompletelyInvalidIDвернуться? Не удалось, так как названный ресурс /GoalTree/GetById?versionId=CompletelyInvalidIDбыл буквально не найден.

2
Отлично, теперь обсуждение перешло от вашей работы к интернету! Это невозможно остановить сейчас!
Карлос Кампдеррос

Ответы:


80

В случае сомнений обратитесь к документации . Просмотр определений W3C для кодов состояния HTTP дает нам следующее:

200 OK - Запрос успешно выполнен. Информация, возвращаемая с ответом, зависит от метода, используемого в запросе.

404 Not Found - сервер не нашел ничего, соответствующего Request-URI.

В контексте вашего API это очень сильно зависит от того, как создаются запросы и как извлекаются объекты. Но моя интерпретация всегда была такой:

  • Если я запрашиваю конкретный объект, и он существует, 200код возврата , если он не существует, возвращает правильный 404код.
  • Но если я запрашиваю набор объектов, соответствующих запросу, нулевой набор является допустимым ответом, и я хочу, чтобы он возвращался с 200кодом. Основанием для этого является то, что запрос был действительным, он успешно выполнен, и запрос ничего не возвратил.

Таким образом, в этом случае вы правы , служба не ищет «конкретную вещь», она запрашивает определенную вещь, если эта вещь не найдена, скажите это ясно.

Я думаю, что Википедия говорит об этом лучше всего:

200 OK - ... Фактический ответ будет зависеть от используемого метода запроса. В запросе GET ответ будет содержать объект, соответствующий запрашиваемому ресурсу.

404 Not Found - Запрошенный ресурс не найден, но может быть снова доступен в будущем. Последующие запросы клиента допустимы.

Кажется, довольно ясно для меня.

По поводу примеров запросов

/GoalTree/GetByDate?versionDate=...
/GoalTree/GetById?versionId=...

Вы сказали, что для формата вы всегда возвращаете ближайшую редакцию к этой дате. Он никогда не вернет объект, поэтому он всегда должен возвращаться 200 OK. Даже если бы это было в состоянии принять диапазон дат, и логика заключалась в том, чтобы вернуть все объекты в течение этого периода, возвращая 200 ОК - 0 Результаты в порядке, так как именно для этого был запрос - набор вещей, которые удовлетворяли этим критериям.

Однако последний отличается тем, что вы запрашиваете конкретный объект , предположительно уникальный, с этой идентичностью. Возвращение 200 OKв этом случае неверно, так как запрошенный ресурс не существует и не найден .

Относительно выбора кодов статуса

  • Коды 2хх Скажите UA, что он правильно сделал , запрос сработал. Это может продолжать делать это в будущем.
  • Коды 3хх Скажите агенту UA, что вы спрашивали, возможно, раньше работало, но теперь это в другом месте. В будущем UA может рассмотреть возможность перехода на перенаправление .
  • Коды 4хх Скажите агенту UA, что он сделал что-то не так , запрос, который он создал , некорректен и не должен пытаться повторить его, по крайней мере, без каких-либо изменений.
  • Коды 5xx Скажите UA, что сервер как-то сломан . Но, эй, этот запрос может сработать в будущем, поэтому нет причин не повторять его снова. (за исключением 501, который более 400 выпусков).

Вы упомянули в комментарии, используя код 5xx, но ваша система работает. Был задан запрос, который не работает и должен сообщить об этом UA. Независимо от того, как вы это нарезаете, это территория 4хх.

Рассмотрим инопланетянина, опрашивающего нашу солнечную систему

Чужой: Компьютер, расскажи мне все планеты, на которых живут люди.

Компьютер: 1 результат найден. земной шар

Чужой: Компьютер, расскажите, пожалуйста, о Земле .

Компьютер: Земля - ​​в основном безвредный.

Чужой: Компьютер, расскажи мне обо всех планетах, которые обитают люди, за пределами пояса астероидов.

Компьютер: 0 найдено.

Чужой: Компьютер, пожалуйста, уничтожь Землю.

Компьютер: 200 ОК.

Чужой: Компьютер, расскажите, пожалуйста, о Земле .

Компьютер: 404 - не найден

Чужой: Компьютер, расскажи мне все планеты, на которых живут люди.

Компьютер: 0 найдено.

Чужой: Победа для могущественной Империи Иркен!


4
+1 Это не запрос, который не возвращает результатов. Это все равно, что запросить у браузера известную веб-страницу и не найти ее. Для чего именно 404.
Андрес Ф.

2
@ imel96 вы забыли, что строка запроса является частью URL.
Лоик Фор-Лакруа

1
@LegoStormtroopr Ваш забавный пример «пришельцев» работает, потому что вселенная НЕ недействительна, когда Земля не существует. Но согласно объяснению ОП, его система должна включать дерево. Без дерева система не работает.
Андрес Ф.

1
@LegoStormtroopr представьте таблицу базы данных. Вы запрашиваете таблицу, иногда вы получаете результат, иногда нет. Таблица - это ваш ресурс, он всегда там, независимо от того, возвращает он строки или нет. Таблица идентифицируемая, у нее есть имя (как у http-ресурсов есть URI). Строк нет, они соответствуют только некоторым параметрам. Даже в базе данных, если вы сделаете обновление, которое ничего не соответствует, вы получите «OK, 0 строк затронуты».
imel96

2
@LegoStormtroopr у вас уже есть ответ. Если они хотят переназначить / GoalTree / GetById? VersionId = x, то он должен вернуть 301 с заголовком Location, установленным в / GoalTree / Id / x.
imel96

11

Игнорируя тот факт, что / GoalTree / Get * выглядит как глагол, а не ресурсы, вы всегда должны возвращать 200, потому что URI / GoalTree / Get * представляют ресурсы, которые всегда доступны для доступа, и это не ошибка клиента, если в результате нет дерева запрос. Просто верните 200 с пустым набором, когда нет сущности, которую нужно вернуть.

Вы используете 404, если ресурс не найден, а не когда нет объекта.

Иными словами, если вы хотите вернуть 404 для ваших объектов, то присвойте им свои собственные URI.


1
Хм. Это имеет смысл. 404 - ошибка пользователя , но, как объясняет OP, это на самом деле системная ошибка; запрос пользователя совершенно действителен! Я не согласен, что 200 - правильный ответ, потому что «нет дерева» - это ошибка .
Андрес Ф.

@ imel96 Я бы предпочел, чтобы действительные сущности всегда возвращались вместо пустых / код состояния 4xx / 5xx. Если бы это был только я, я бы вернул действительную сущность, как например вики. Меньше головной боли не нужно обрабатывать ошибки. Как бы то ни было, я бы сказал, что это почти как 500. Система находится в неопределенном состоянии, это не должно произойти. И возвращение ОК не имеет смысла. 404 относительно РФС также не имеет смысла. Так что, когда ничего не имеет смысла ... только 500 имеет смысл!
Лоик Фор-Лакруа

@Sybiam хорошо, вы попросили http код состояния, который очень хорошо определен. В связи с этим, даже если ваша бизнес-логика говорит, что это ошибка, это не означает, что система как http-сервер имеет ошибку. В вашем случае он понял ваш запрос, обработал ваш запрос и просто получилось, что в результате нет сущности. Таким образом, вы также не можете использовать 500. По крайней мере, подумайте над тем, чтобы дать вашим объектам надлежащие URI и посмотреть, имеет ли смысл rfc или нет.
imel96

+1 Если у вас был REST API (у каждой сущности был свой собственный путь), то вы могли бы вернуть 404, но ваши пути - глаголы и всегда будут найдены.
Стоп Harm Monica

@OrangeDog: /GoalTree/GetById?versionId=12345 это очень хороший URI (ну, по крайней мере, относительный), который идентифицирует конкретный ресурс, а именно данные, соответствующие идентификатору версии 12345в системе. Если данных с таким идентификатором не существует, HTTP-ответ 404 является абсолютно подходящим. Конечно, тело ответа должно, в любом случае, содержать правильно отформатированный ответ (например, JSON, если этого ожидают типичные клиенты, запрашивающие такие ресурсы), указывающий конкретную природу и причину ошибки.
Илмари Каронен

7

Это интересный вопрос, потому что все дело в спецификации системы.

Ответ imel96 убедил меня, что 404 не будет правильным ответом, поскольку семейство кодов 4xx в основном предназначено для ошибок пользователя / клиента , а это не так. URL правильно сформирован, и дерево должно быть там; если это не так, система находится в противоречивом состоянии!

Поэтому это ошибка сервера , то есть что-то в семействе 5xx. Возможно, общая внутренняя ошибка сервера 500 или служба 503 недоступна (служба «принеси мне дерево, которое должно быть там»).


2
Не правда, пользователь ошибся, потому что он попросил что-то, что не существует .

@LegoStormtroopr Запрос о том, чего не существует, не всегда является ошибкой. Если вы запрашиваете сетевой ресурс, а сеть не работает, значит, это сетевая ошибка.
Андрес Ф.

1
@LegoStormtroopr Кроме того, дерево должно существовать; система не может функционировать без него, согласно объяснению ОП. Таким образом, можно запросить этот ресурс; если его там нет, это должна быть системная (или серверная) ошибка.
Андрес Ф.

2
@Sybiam Если вы собираетесь идти по пути кода 5xx, 503 будет «503 Сервис недоступен - сервер в настоящее время не может обработать запрос из-за временной перегрузки или обслуживания сервера». Ваш сервер не перегружен, запрос не найден. Кроме того, коды 5xx предназначены для случаев, когда «сервер знает, что он допустил ошибку или не может выполнить запрос»

1
@AndresF. Если честно, код 500, вероятно, хорошо. Учитывая, как вопрос изменился с течением времени, он будет работать. В основном, я просто выступаю против возвращения 200, если все не в порядке.

6

Я бы сказал, что код ответа 200 или 404 может быть действительным , в зависимости от того, как вы смотрите на ситуацию.

Дело в том, что коды ответов HTTP определяются в контексте сервера , который может доставлять различные ресурсы на основе их URL. В этом контексте значения 200 OKи 404 Not Foundсовершенно недвусмысленны: первый говорит: «вот ресурс, который вы просили», а второй - «извините, у меня нет такого ресурса».

Однако в вашей ситуации у вас есть дополнительный прикладной уровень между HTTP-сервером и реальными ресурсами (деревьями), которые запрашиваются. Приложение занимает своего рода промежуточное пространство, которое не очень хорошо рассматривается в спецификации HTTP.

С точки зрения веб - сервер, заявка выглядит вид на как ресурс: это обычно файл на сервере, которые были определены (часть) URL - адрес, так же как и другие ресурсы (например , файлы статические) сервер может служить. С другой стороны, это странный вид ресурса, поскольку он состоит из исполняемого кода, который динамически определяет содержимое и, возможно, даже код состояния ответа, заставляя его вести себя в некотором роде как мини-сервер.

В частности, в вашем примере, веб-сервер может найти приложение очень хорошо, но затем приложение не может найти подресурс (дерево), который был запрошен. Теперь, если вы считаете, что приложение является просто расширением сервера , а подэлемент (дерево) - фактическим ресурсом, тогда уместен ответ 404 : сервер просто делегировал задачу поиска фактического ресурса приложению который, в свою очередь, не смог этого сделать.

С другой стороны, если ваша точка зрения заключается в том, что приложение является запрашиваемым ресурсом , то, очевидно, веб-сервер должен вернуть ответ 200 ; В конце концов, приложение было найдено и выполнено правильно. Очевидно, что в этом случае приложение должно фактически вернуть действительное тело ответа в ожидаемом формате, указывая (используя любой протокол более высокого уровня, который кодирует этот формат), что фактические данные, соответствующие запросу, не были найдены.

Обе эти точки зрения могут иметь смысл. В большинстве случаев , по крайней мере, для приложений, предназначенных для прямого доступа через HTTP с помощью обычного веб-браузера, я бы предпочел первое представление : пользователь, как правило, не заботится о внутренних деталях, таких как разница между сервером и приложением, он просто заботиться о том, есть ли данные, которые они хотели, есть или нет.

Однако в конкретном случае приложения, предназначенного для связи с другими компьютерными программами, использующими настраиваемый высокоуровневый API-протокол и использующий только HTTP в качестве низкоуровневого транспортного уровня , можно привести аргумент в пользу последнего представления : клиенты, взаимодействующие с таким приложением, на уровне HTTP все, что их действительно волнует , это то, удалось ли им успешно связаться с приложением или нет. Все остальное в таких случаях часто более естественно сообщается с использованием протокола более высокого уровня.


В любом случае, независимо от того, какое из представленных выше представлений вы предпочитаете, есть несколько деталей, о которых следует помнить. Одна из них заключается в том, что во многих случаях может быть значимое различие между (по существу) пустым ресурсом и несуществующим .

На уровне HTTP пустой ресурс будет просто указываться кодом ответа 200 и пустым телом ответа, тогда как несуществующий ресурс будет указываться ответом 404 и телом ресурса, объясняющим отсутствие ресурса. В высокоуровневом API-протоколе обычно указывается несуществующий ресурс с помощью ответа об ошибке, содержащего подходящий код / ​​сообщение об ошибке, зависящее от протокола, тогда как пустой ответ будет просто нормальной структурой ответа без элементов данных.

(Обратите внимание, что ресурс не должен быть буквально нулевым байтом длиной, чтобы быть «пустым» в том смысле, который я имею в виду выше. Например, результат поиска без соответствующих элементов будет считаться пустым в широком смысле, как и результат запроса SQL с нет строк или XML-документ, не содержащий фактических данных.)

Кроме того , конечно же , если приложение действительно считает , что запрашиваемая subresource должна быть там, но не может его найти, то третий возможный код ответа существует: 500 Internal Server Error. Такой ответ имеет смысл, если наличие ресурса является предполагаемой предпосылкой для приложения, так что его отсутствие обязательно указывает на внутреннюю неисправность.

Наконец, вы всегда должны помнить закон Постеля :

« Будьте консервативны в том, что вы посылаете, и либеральны в том, что вы получаете ».

Если сервер должен реагировать в той или иной ситуации , с 200 или 404 ответ, это не оправдывает вас как клиента Implementor от обработки либо ответа надлежащим образом и способом , который максимизирует надежную совместимость. Конечно, можно утверждать, что означает «подходящая» обработка в различных ситуациях, но обычно она не должна включать сбой или иное «развал».


Дилемма хорошо объяснила.
Марсель

Там нет дилеммы. этот ответ не основан на том, какой ресурс определен как в соответствующем rfc. см. мой комментарий под ответом @LegoStormtroopr.
imel96

@ imel96: Я думаю, что вы неправильно интерпретируете RFC 1630: абзац, который вы цитируете в своем предыдущем комментарии, полностью читает: «Знак вопроса («? », ASCII 3F hex) используется для разграничения границы между URI запрашиваемого объект и набор слов, используемых для выражения запроса к этому объекту. Когда эта форма используется, объединенный URI обозначает объект, который является результатом запроса, примененного к исходному объекту. " (выделено мной). Таким образом, ясно, что строка запроса действительно является частью URI (даже если часть перед строкой запроса сама по себе также является действительным URI) ...
Ильмари Каронен

... и что этот объединенный URI идентифицирует конкретный ресурс, который клиент может запросить, отправив этот URI на сервер. В любом случае RFC 2616 (HTTP) просто определяет ресурс как «объект или службу сетевых данных, которые могут быть идентифицированы посредством URI, как определено в разделе 3.2». и продолжает: «Что касается HTTP, то универсальные идентификаторы ресурсов - это просто отформатированные строки, которые идентифицируют - через имя, местоположение или любую другую характеристику - ресурс».
Илмари Каронен

@ IlmariKaronen ты прав. Я перепутал HTTP с REST. Тем не менее, все еще кажется неправильным, потому что я не уверен, что вы можете сделать с ресурсом с URI, таким как / GoalTree / Get? VersionDate = 2000BC
imel96

3

Как насчет 204 Нет контента? Предполагается, что ваш запрос был успешно обработан, но ничего не возвращается. Это все еще «успех», но позволяет вам увидеть, есть ли у вас результаты, основанные только на коде состояния.


6
если вы читаете спецификацию далее, «Этот ответ в первую очередь предназначен для того, чтобы разрешить ввод действий, не вызывая изменений в активном представлении документа агента пользователя». Так что это не должно использоваться для запросов GET.
imel96

3

Если URL представляет ресурс, который никогда не существовал, верните 404 Not Found

Если URL представляет ресурс, который является пустым списком, верните пустой список и 200 OK.

Пример:

{
  total: 0,
  items: []
}

Если URL представляет ресурс, который раньше существовал, верните 410 Gone.

Относительно диалога Lego Stormtrooper:

Alien: Computer, please tell me all planets that humans inhabit. GET /planets?inhabitedBy=humans

Computer: 200 OK. { total: 1, items:[{name:'Earth'}] }

Alien: Computer, please tell me about Earth. GET /planets/earth

Computer: 200 OK. {name:'Earth', status: 'Mostly Harmless'}

Alien: Computer, please tell me about all planets humans inhabit, outside the asteroid belt. GET /planets?inhabitedBy=humans&distanceFromSun=lots

Computer: 200 OK. {total:0, items:[] }

Alien: Computer, please destroy Earth. DELETE /planets/earth

Computer: 204 No Content. (or 202 Accepted if it takes some time to destroy Earth)

Alien: Computer, please tell me about Earth. GET /planets/earth

Computer: 410 Gone

Alien: Computer, please tell me all planets that humans inhabit. GET /planets?inhabitedBy=humans

Computer: 200 OK 0 {total: 0, items:[] }

Alien: Victory for the mighty Irken Empire!

1

Судя по всему, это API для внутреннего использования. Это дает преимущество при использовании любой схемы, которая дает наибольшую выгоду , независимо от того, является ли она индивидуальной (спецификацией) или нет. Это не означает, что вы должны придумывать свои собственные коды состояния, но это нормально - немного «согнуть» правила, если это выгодно.

Я согласен с вашей позицией, что вы должны получить код состояния, который показывает, что что-то пошло не так. В конце концов, для чего нужны коды состояния. Также вы получаете выгоду от библиотек, которые генерируют исключения / и т.д. на код состояния не-200, поэтому вам не нужно явно проверять (или вы можете написать свою собственную обертку, которая делает это).

Я также согласен с точкой зрения Андреса Ф., что 500 подходит, поскольку дерево должно существовать. Однако на практике мне нравится разбивать ошибки сервера на две категории. Что-то неожиданное пошло не так, а то, что я могу практически проверить, пошло не так. Это приводит к следующим кодам состояния,

  • 200 - все хорошо
  • 404 - Неправильный URL
  • 409 - что-то пошло не так
  • 500 - на сервере произошла непредвиденная ошибка

В вашем конкретном случае вы можете проверить, существует ли дерево на стороне сервера и, если его нет, вернуть 409. Это ожидаемая ошибка (вы знаете, что это может произойти, вы можете проверить ее и т. Д.) , Конфликт 409 - это только мое личное предпочтение, 5хх также может быть уместным, если вы можете сесть и решить это с вашей командой.

Распределение по категориям кодов помогает быстрее определить тип ошибки, но может иметь преимущества помимо организации. Часто с ошибками веб-сайта вы не хотите, чтобы клиент получал неожиданные ошибки, так как это может быть проблемой безопасности и выявлять уязвимости, поэтому вы возвращаете универсальный 500 «Произошла ошибка». и зарегистрируйте полную ошибку на сервере. Но если ожидаемая ошибка возникает как 409, вы знаете, что было бы безопасно показать ошибку клиенту, и вам не нужно оставлять их в неведении относительно того, что произошло. Это только одно практическое использование, которое я могу рассказать, но есть много возможностей.

Это что-то вроде уловки 22, потому что вы публикуете это из-за невозможности договориться со своими коллегами, но, похоже, вы, ребята, больше спорите о семантике и о том, кто политически корректен. На самом деле не имеет значения, кто является более подходящим, если вы можете придумать систему, которая приносит наибольшую пользу компании.

С другой стороны, если это общедоступный API, следующий спецификациям как можно ближе, было бы более важно, чтобы избежать путаницы среди сообщества.


0

Возьмите тангенциальный удар по этому вопросу: если человек в конечном итоге использует API (через графический интерфейс), я бы предложил сделать все, что облегчает жизнь конечного пользователя. Отсутствие дерева, когда оно должно существовать, является ошибкой «несоответствия модели домена». Системная ошибка - это когда у вас закончилась память или произошел какой-либо другой системный сбой. Поэтому возвращать 5xx неуместно. Как уже упоминалось несколькими людьми выше, 4xx может быть уместным, если само дерево имеет свой собственный URI, что здесь не так. Но вот что 404 говорит клиенту: вы можете пытаться снова и снова, пока не получите что-то обратно. Если вы вернули 200, вы могли бы вернуть достаточное количество диагностических данных обратно пользователю или пользовательскому агенту, чтобы пользовательский агент мог отображать значение, чтобы пользователь прекратил повторные попытки и просто обратился в службу поддержки. С другой стороны, если этот API предназначен только для систем,


Все 404 на самом деле говорят: «Этого здесь нет, и я не знаю, где это». 3xx и 5xx подходят для повторной попытки. Но 4xx говорит: «Ваш текущий запрос был недостаточен для того, чтобы я нашел для вас что-нибудь полезное. Пожалуйста, уходите».

Мне нравится возможность различать URL NOT FOUND и Resource NOT FOUND ... Таким образом, конечная точка службы запущена и работает 200, однако запрошенный ресурс НЕ НАЙДЕН 404 (тело ответа).
Лимонад
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.