Пейджинг в коллекции отдыха

134

Я заинтересован в предоставлении прямого интерфейса REST коллекциям документов JSON (например, CouchDB или Persevere ). Проблема, с которой я сталкиваюсь, состоит в том, как обработать GETоперацию в корне коллекции, если коллекция большая.

В качестве примера притворяюсь, что я выставляю Questionsтаблицу StackOverflow, где каждая строка представлена ​​как документ (не обязательно, что такая таблица обязательно есть, просто конкретный пример значительного набора «документов»). Коллекция будет доступна в /db/questionsс обычным CRUD апи GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questionsнаходится в игре. Стандартный способ получить всю коллекцию состоит в GET /db/questionsтом, чтобы, но если это наивно сбрасывает каждую строку как объект JSON, вы получите довольно значительную загрузку и много работы со стороны сервера.

Решение, конечно, пейджинговое. Dojo решил эту проблему в своем JsonRestStore с помощью умного, совместимого с RFC2616 расширения использования Rangeзаголовка с пользовательским блоком диапазона items. В результате 206 Partial Contentвозвращается только запрошенный диапазон. Преимущество этого подхода перед параметром запроса состоит в том, что он оставляет строку запроса для ... запросов (например, GET /db/questions/?score>200или, например , да, которые будут закодированы %3E).

Этот подход полностью охватывает поведение, которое я хочу. Проблема в том, что RFC 2616 указывает, что в ответе 206 (выделено мое):

Запрос должен быть включен в поле заголовка Range ( раздел 14.35 ) , указывающее желаемый диапазон, и может включить поле заголовка If-Range ( раздел 14.27 ) , чтобы сделать запрос условным.

Это имеет смысл в контексте стандартного использования заголовка, но является проблемой, потому что я хотел бы, чтобы ответ 206 был по умолчанию для обработки наивных клиентов / случайных людей, которые исследуют.

Я подробно рассмотрел RFC в поисках решения, но был недоволен моими решениями и заинтересован в том, чтобы SO взялся за проблему.

У меня были идеи:

  • Вернуться 200с Content-Rangeзаголовком! - Я не думаю, что это неправильно, но я бы предпочел, чтобы более очевидный показатель того, что ответом является только частичное содержание.
  • Возврат400 Range Required - специального кода ответа 400 для требуемых заголовков не существует, поэтому ошибка по умолчанию должна использоваться и считываться вручную. Это также затрудняет исследование через веб-браузер (или другой клиент, такой как Resty).
  • Использовать параметр запроса - стандартный подход, но я надеюсь разрешить запросы а-ля Persevere, и это врезается в пространство имен запроса.
  • Просто вернись 206! - Я думаю, что большинство клиентов не пугались бы, но я бы предпочел не идти против MUST в RFC
  • Расширьте спецификацию! Возврат266 Partial Content - ведет себя точно так же, как 206, но отвечает на запрос, который НЕ ДОЛЖЕН содержать Rangeзаголовок. Я считаю, что 266 достаточно высока, чтобы не сталкиваться с проблемами столкновения, и это имеет для меня смысл, но я не уверен, считается ли это табу или нет.

Я думаю, что это довольно распространенная проблема, и я хотел бы, чтобы это было сделано неким де-факто способом, чтобы я или кто-то еще не изобретал велосипед.

Каков наилучший способ выставить полную коллекцию через HTTP, если коллекция большая?

Карл Гертин
источник
21
Ух ты, это хороший пример вопроса, в котором раньше были серьезные размышления.
Хайко Рупп
1
Что касается подхода Dojo к использованию заголовка Range, хотя Accept-Ranges допускает расширение, то, насколько я могу судить, EBNF для Range этого не делает: tools.ietf.org/html/rfc2616#section-14.35.2 . Спецификация указывает, Range = "Range" ":" ranges-specifierгде последний в tools.ietf.org/html/rfc2616#section-14.35.1 описывается просто как «спецификатор байтовых диапазонов», который должен начинаться с «байтовой единицы», которая определяется как строка «байтов». ».
Бретт Замир
2
Content-RangeЗаголовок относится к телу (можно использовать с запросом при загрузке больших файлов и т.д., или для ответа при загрузке). RangeЗаголовка используется для запроса определенного диапазона. Нужно ответить, 206когда Rangeзаголовок был включен в запрос. Если это не так, ответ может по-прежнему включать Content-Rangeзаголовок, но код ответа должен быть 200. Этот заголовок на самом деле кажется идеальным для подкачки.
Стейн де Витт
Но сам RFC 2616 говорит, что «реализации HTTP / 1.1 МОГУТ игнорировать диапазоны, указанные с использованием других модулей». Так стоит ли использовать заголовки Range для разбивки на страницы? Потому что это может поставить под угрозу совместимость.
Четан Чулвар

Ответы:

23

Мне кажется, что расширения диапазона HTTP не предназначены для вашего случая использования, и поэтому вам не следует пытаться. Частичный ответ подразумевает 206, и 206должен быть отправлен, только если клиент попросил его.

Возможно, вы захотите рассмотреть другой подход, например, тот, который используется в Atom (где представление по проекту может быть частичным, и возвращается со статусом 200и, возможно, ссылками на пейджинг). См. RFC 4287 и RFC 5005 .

Джулиан Решке
источник
14
Использование Dojo полностью в пределах спецификации. Если сервер не понимает itemsединицы измерения диапазона, он возвращает полный ответ. Я знаком с Atom, но это не общее решение для пейджинга Rest. Это не решение для отдельного случая, а скорее общее решение. Не все документы / коллекции соответствуют модели Atom, и нет причин для ее форсирования, если это не требуется.
Карл Гертин
1
@KarlGuertin Согласен. Жаль, что это общепринятый ответ, потому что кажется, что многие в сообществе на самом деле обнимаются Rangeи Content-Rangeдля целей подкачки страниц.
Стейн де Витт
34

Я не очень согласен с некоторыми из вас, ребята. Я работал в течение нескольких недель над этими функциями для моей службы REST. То, что я в итоге сделал, действительно просто. Мое решение имеет смысл только для того, что REST люди называют коллекцией.

Клиент ДОЛЖЕН включать заголовок «Range», чтобы указать, какая часть коллекции ему нужна, или иным образом быть готовым обработать ошибку 413 REQUESTED ENTITY TOO LARGE, когда запрашиваемая коллекция слишком велика, чтобы ее можно было извлечь за один цикл.

Сервер отправляет ответ 206 ЧАСТИЧНОЕ СОДЕРЖАНИЕ, в котором заголовок Content-Range указывает, какая часть ресурса была отправлена, и заголовок ETag для идентификации текущей версии коллекции. Я обычно использую ETag {last_modification_timestamp} - {resource_id}, подобный Facebook, и считаю, что ETag коллекции - это самый последний измененный ресурс, который она содержит.

Чтобы запросить определенную часть коллекции, клиент ДОЛЖЕН использовать заголовок «Range» и заполнить заголовок «If-Match» ETag коллекции, полученной из ранее выполненных запросов, для получения других частей этой же коллекции. Поэтому сервер может проверить, что коллекция не изменилась перед отправкой запрошенной части. Если существует более поздняя версия, возвращается ответ 412 PRECONDITION FAILED, чтобы пригласить клиента получить коллекцию с нуля. Это необходимо, поскольку это может означать, что некоторые ресурсы могли быть добавлены или удалены до или после запрашиваемой в данный момент части.

Я использую ETag / If-Match в тандеме с Last-Modified / If-Unmodified-Since, чтобы оптимизировать кэш. Браузеры и прокси-серверы могут полагаться на один или оба из них для своих алгоритмов кэширования.

Я думаю, что URL должен быть чистым, если только он не включает поисковый запрос / фильтр. Если вы думаете об этом, поиск - это не более чем частичное представление коллекции. Вместо машин / поиска? Q = URL-адреса типа BMW, мы должны увидеть больше автомобилей? Изготовителя = BMW.

Mohamed
источник
Вы имели в виду 416 «Запрошенный диапазон неудовлетворительный» или «413» Слишком большой объект запроса?
1
@ Mohamed Я думаю, вы имеете в виду If-Unmodified-Since, что соответствует варианту E-Tag If-Match, а не If-Modified-Since. Тем не менее, вы можете также рассмотреть возможность удаления этого ограничения, в зависимости от вашего варианта использования. Скажем, у вас есть коллекция, которая растет только сверху (как, например, некоторая «новая первая» коллекция стилей), худшее, что может произойти, если эта коллекция изменит промежуточные запросы, - это то, что пользователь, просматривающий коллекцию, видит записи дважды. (Что само по себе также является полезной информацией: оно сообщает пользователю, что коллекция изменилась)
Евгений Бересовский
20
413 означает «Запросить слишком большой объект», а не «Запрошенный слишком большой объект». Это означает, что размер вашего запроса, например, при загрузке файла, больше, чем сервер готов обработать. Так что использовать его для этого, похоже, не совсем уместно.
user247702
@ Mohamed Я знаю, что это старый вопрос, но если ETag коллекции является ETag последнего измененного ресурса, который содержит коллекция, какое значение заголовка If-Match следует использовать при изменении одного ресурса в коллекции? Использование значения ETag, возвращенного с коллекцией, является неправильным, поскольку клиент сможет изменить ресурс, даже если он не видит последнее состояние ресурса.
Микаэль Маррач
8
Я категорически не согласен с использованием 413. Это код ошибки, означающий, что клиент отправляет что-то, что сервер отказывается принять из-за размера. А не наоборот! См. Tools.ietf.org/html/rfc7231#section-6.5.11 (обратите внимание, что в нем говорится, что полезная нагрузка запроса . Не полезная нагрузка ответа )!
exhuma
7

Вы все еще можете вернуться Accept-Rangesи Content-Rangesс 200кодом ответа. Эти два заголовка ответа дают вам достаточно информации, чтобы вывести ту же информацию, которую 206код ответа предоставляет явно.

Я хотел бы использовать Rangeдля нумерации страниц, и он просто вернуть 200для равнины GET.

Это чувствует себя на 100% RESTful и не делает просмотр более сложным.

Изменить: я написал в блоге об этом: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

Джон Гитцен
источник
5

Если имеется более одной страницы ответов, и вы не хотите предлагать всю коллекцию одновременно, означает ли это, что существует несколько вариантов?

По запросу /db/questionsвернитесь 300 Multiple Choicesс Linkзаголовками, которые указывают, как добраться до каждой страницы, а также с объектом JSON или страницей HTML со списком URL-адресов.

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

У вас будет один Linkзаголовок для каждой страницы результатов (пустая строка означает текущий URL-адрес, а URL-адрес один и тот же для каждой страницы, доступ к которому осуществляется только с разными диапазонами), а отношение определяется как настраиваемый для будущей Linkспецификации. , Эти отношения объяснят ваш обычай 266или ваше нарушение 206. Эти заголовки являются вашей машиночитаемой версией, так как все ваши примеры в любом случае требуют понимания клиента.

(Если вы придерживаетесь маршрута «range», я считаю, что ваш собственный 2xxкод возврата, как вы его описали, будет лучшим поведением здесь. От вас ожидают, что вы сделаете это для своих приложений, и такие [»коды состояния HTTP являются расширяемыми. "], и у вас есть веские причины.)

300 Multiple Choicesговорит, что вы ДОЛЖНЫ также предоставить телу способ выбора для пользовательского агента. Если ваш клиент понимает, он должен использовать Linkзаголовки. Если это пользователь, просматривающий вручную, может быть, HTML-страница со ссылками на специальный «выгружаемый» корневой ресурс, который может обрабатывать отображение этой конкретной страницы на основе URL-адреса? /humanpage/1/db/questionsили что-то отвратительное?


Комментарии к сообщению Ричарда Левассера напоминают мне о дополнительной опции: Acceptзаголовок (раздел 14.1). Когда появилась спецификация oEmbed, я удивился, почему она не была полностью реализована с использованием HTTP, и написал альтернативную версию с их использованием.

Держите 300 Multiple Choices, на Linkзаголовки и страницу HTML для исходного наивным HTTP GET, но вместо диапазонов использования, есть новые отношения пейджинга определяют использование Acceptзаголовка. Ваш следующий HTTP-запрос может выглядеть так:

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

AcceptЗаголовок позволяет определить приемлемый тип контента (ваш JSON возврат), а также расширяемые параметры для данного типа (ваш номер страницы). Судя по моим заметкам из моей записи oEmbed (здесь я не могу ссылаться на них, я перечислю их в своем профиле), вы можете быть очень откровенными и предоставить здесь версию спецификации / отношения, если вам нужно переопределить значение этого pageпараметра в будущем.

Vitorio
источник
1
Заголовки ссылок +1, но я бы также порекомендовал общие first, prev, next, last rels, а также prev-archive, next-archive и current в RFC5005.
Джозеф Холстен
> При запросе к / db / questions верните 300 множественных вариантов с заголовками ссылок, которые указывают, как добраться до каждой страницы [..] Проблема с этим (и с большинством чистых конструкций REST) ​​заключается в том, что он убивает за задержку. Цель состоит в том, чтобы минимизировать сетевые запросы. Этот первый запрос должен дать результаты, а не ссылки на дополнительные запросы, которые в конечном итоге дадут необходимые нам данные.
Стейн де Витт
4

Редактировать:

Подумав об этом немного больше, я склонен согласиться с тем, что заголовки Range не подходят для нумерации страниц. Логика заключается в том, что заголовок Range предназначен для ответа сервера, а не для приложений. Если вы предоставили 100 мегабайт результатов, но сервер (или клиент) мог обрабатывать только 1 мегабайт за раз, то для этого и существует заголовок Range.

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

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


Я думаю, что вы ответили на свой вопрос, более или менее - верните 200 или 206 с диапазоном содержимого и при необходимости используйте параметр запроса. Я бы понюхал пользовательский агент и тип контента и, в зависимости от них, проверил параметр запроса. В противном случае требуются заголовки диапазона.

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

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

Конечно, есть также дебаты о том, должен ли URL содержать все необходимое состояние для такого рода вещей. Указание диапазона с использованием заголовков может считаться некомфортным.

Кроме того, было бы неплохо, если бы серверы могли ответить заголовком «Can-Specify: Header1, header2», а веб-браузеры предоставили бы пользовательский интерфейс, чтобы пользователи могли заполнять значения, если они этого пожелают.

Ричард Левассер
источник
Спасибо за ответ. Я думал об этой теме, но надеялся получить второе мнение. Бывает ли иметь указатель на аргументы заголовка?
Карл Гертин
Вот единственный, который я добавил в закладки (см. Обсуждение в комментариях): barelyenough.org/blog/2008/05/versioning-rest-web-services Еще один сайт, посвященный использованию в Ruby .json, .xml,. Что угодно при определении тип содержимого запроса. Вот некоторые примеры: * язык - если указать URL-адрес, отправка ссылки в другую страну приведет к неверному языку. * разбиение на страницы - Помещение в заголовок означает, что вы не можете связать людей с тем, что вы видите
Ричард Левассер
* content-type: сочетание проблем языка и нумерации страниц - если это в URL, что, если клиент не поддерживает этот тип контента (например, расширение .ajax и .html)? И наоборот, без этого типа содержимого в URL-адресе вы не сможете обеспечить то же представление. "новый сайт ajax! example.com/cool.ajax" против "классной статьи здесь: example.com/article.ajax#id=123".
Ричард Левассер
2
ИМО, идет ли он в URL или нет, зависит от того, что это такое. Мое общее правило: если он идентифицирует конкретный ресурс (будь то ресурс в определенном состоянии, выбор ресурсов или дискретный результат), он указывается в URL. Поисковые запросы, разбиение на страницы и спокойные транзакции являются хорошими примерами этого. Если это то, что нужно для преобразования абстрактного представления в конкретное представление, оно идет в заголовке. auth info и content-type являются хорошими примерами этого.
Ричард Левассер
Я думаю о строке запроса в URL как об опциях запроса указанного ресурса.
wprl
3

Вы можете подумать об использовании модели, похожей на Atom Feed Protocol, поскольку в ней есть разумная HTTP-модель коллекций и способы манипулирования ими (где безумный означает WebDAV).

Существует протокол публикации Atom, который определяет модель коллекции и операции REST, плюс вы можете использовать RFC 5005 - Пейджинг и архивирование каналов для просмотра больших коллекций.

Переход с Atom XML на контент JSON не должен влиять на идею.

dajobe
источник
3

Я думаю, что реальная проблема заключается в том, что в спецификации нет ничего, что объясняет нам, как выполнять автоматическое перенаправление при столкновении с 413 - Requested Entity Too Large.

Недавно я боролся с этой же проблемой и искал вдохновение в книге RESTful Web Services . Лично я не думаю, что 206 подходит из-за требования заголовка. Мои мысли также привели меня к 300, но я подумал, что это было больше для разных типов пантомимы, поэтому я посмотрел, что Ричардсон и Руби должны были сказать по этому вопросу в Приложении B, стр. 377. Они предлагают, чтобы сервер просто выбрал предпочтительный представление и отправить его обратно с 200, в основном игнорируя представление о том, что это должно быть 300.

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

Позже я начал думать, что, может быть, нужно отправить 307 - временное перенаправление на ссылку, которая будет выглядеть примерно так: / db / questions / 1,25 - которая оставляет исходный URI в качестве канонического имени ресурса, но заставляет вас соответствующим образом названный подчиненный ресурс. Такое поведение я бы хотел увидеть из 413, но 307 кажется хорошим компромиссом. На самом деле еще не пробовал это в коде, хотя. Что было бы еще лучше, так это перенаправление для перенаправления на URL-адрес, содержащий фактические идентификаторы последних заданных вопросов. Например, если каждый вопрос имеет целочисленный идентификатор, и в системе имеется 100 вопросов, и вы хотите отобразить десять самых последних, запросы к / db / questions должны быть с 307 до / db / questions / 100,91.

Это очень хороший вопрос, спасибо, что задали его. Вы подтвердили мне, что я не схожу с ума от того, что провел дни, думая об этом.

stinkymatt
источник
303 будет лучше в этом отношении, чем 307. 307 подразумевает, что исходный URL скоро начнет отвечать, как ожидает клиент.
Николас Шенкс
RFC 7231 ссылается на код состояния HTTP 413 как « Слишком большая полезная нагрузка» и связывает этот код с размером запроса, а не с потенциальным размером ответа.
beawolf
1

Вы можете обнаружить Rangeзаголовок и имитировать Dojo, если он присутствует, и имитировать Atom, если его нет. Мне кажется, что это аккуратно разделяет варианты использования. Если вы отвечаете на запрос REST из вашего приложения, вы ожидаете, что он будет отформатирован с Rangeзаголовком. Если вы отвечаете на случайный браузер, то, если вы вернете ссылки на пейджинг, это позволит инструменту легко исследовать коллекцию.

Greg
источник
1

Одна из больших проблем с заголовками диапазонов состоит в том, что многие корпоративные прокси отфильтровывают их. Я бы посоветовал использовать параметр запроса вместо этого.

user64141
источник
1

С публикацией rfc723x , незарегистрированные единицы дальности действительно идут против явной рекомендации в спецификации . Рассмотрим rfc7233 (устарел rfc2616):

« Новые единицы измерения дальности должны быть зарегистрированы в IANA » (вместе со ссылкой на реестр единиц измерения дальности HTTP ).

Сэм
источник
0

Мне кажется, что лучший способ сделать это - включить диапазон в качестве параметров запроса. например, GET / db / questions /? date> mindate & date <maxdate . Получив GET для / db / questions / без параметров запроса, верните 303 с Location: / db / questions /? Query-parameters-to-retrieve-the-default-page . Затем укажите другой URL, по которому тот, кто использует ваш API, получает статистику о коллекции (например, какие параметры запроса использовать, если он / она хочет всю коллекцию);

Дафан
источник
0

Хотя для этой цели можно использовать заголовок Range, я не думаю, что это было целью. Кажется, он был разработан для обработки нестабильных соединений, а также для ограничения данных (поэтому клиент может запросить часть запроса, если чего-то не хватает или размер слишком велик для обработки). Вы взламываете нумерацию страниц на то, что, вероятно, используется для других целей на уровне коммуникации. «Правильный» способ разбивки на страницы - с типами, которые вы возвращаете. Вместо того, чтобы возвращать объект вопросов, вы должны вместо этого возвращать новый тип.

Так что, если вопросы такие:

<questions> <question index=1></question> <question index=2></question> ... </questions>

Новый тип может быть примерно таким:

<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

Конечно, вы контролируете свои типы мультимедиа, поэтому вы можете сделать ваши «страницы» форматом, который соответствует вашим потребностям. Если вы делаете что-то общее, у вас может быть один синтаксический анализатор на клиенте, чтобы обрабатывать пейджинг одинаково для всех типов. Я думаю, что это больше соответствует духу спецификации HTTP, а не обманывает параметр Range для чего-то другого.

jeremyh
источник