Estou interessado em expor uma interface REST direta a coleções de documentos JSON (pense em CouchDB ou Persevere ). O problema que eu estou enfrentando é como lidar com a GET
operação na raiz da coleção, se a coleção for grande.
Como exemplo, finja que estou expondo a Questions
tabela do StackOverflow em que cada linha é exposta como um documento (não que exista necessariamente uma tabela desse tipo, apenas um exemplo concreto de uma coleção considerável de 'documentos'). A coleção seria disponibilizado na /db/questions
com a api CRUD de costume GET /db/questions/XXX
, PUT /db/questions/XXX
, POST /db/questions
está em jogo. A maneira padrão de obter toda a coleção é, GET /db/questions
mas se isso ingenuamente despejar cada linha como um objeto JSON, você terá um download bastante considerável e muito trabalho por parte do servidor.
A solução é, obviamente, a paginação. O Dojo resolveu esse problema em seu JsonRestStore por meio de uma inteligente extensão compatível com RFC2616, usando o Range
cabeçalho com uma unidade de intervalo personalizada items
. O resultado é um 206 Partial Content
que retorna apenas o intervalo solicitado. A vantagem dessa abordagem sobre um parâmetro de consulta é que ele deixa a string de consulta para ... consultas (por exemplo, GET /db/questions/?score>200
ou algo assim, e sim, isso seria codificado %3E
).
Essa abordagem cobre completamente o comportamento que eu quero. O problema é que o RFC 2616 especifica que, em uma resposta 206 (ênfase minha):
O pedido DEVE ter incluído um campo de cabeçalho Range ( seção 14.35 ) indicando o intervalo desejado, e PODE ter incluído um campo de cabeçalho If-Range ( seção 14.27 ) para tornar o pedido condicional.
Isso faz sentido no contexto do uso padrão do cabeçalho, mas é um problema, porque eu gostaria que a resposta 206 fosse o padrão para lidar com clientes ingênuos / pessoas aleatórias explorando.
Analisei detalhadamente a RFC em busca de uma solução, mas fiquei insatisfeito com minhas soluções e estou interessado na opinião do SO sobre o problema.
Idéias que tive:
- Retorne
200
com umContent-Range
cabeçalho! - Não acho que isso esteja errado, mas eu prefiro um indicador mais óbvio de que a resposta é apenas Conteúdo Parcial. - Retorno
400 Range Required
- Não há um código de resposta especial 400 para os cabeçalhos necessários; portanto, o erro padrão deve ser usado e lido manualmente. Isso também dificulta a exploração via navegador da Web (ou outro cliente como o Resty). - Use um parâmetro de consulta - A abordagem padrão, mas espero permitir consultas a la Persevere e isso corta o espaço para nome da consulta.
- Basta voltar
206
! - Eu acho que a maioria dos clientes não iria surtar, mas eu prefiro não ir contra um DEVE na RFC - Estenda a especificação! Retorno
266 Partial Content
- se comporta exatamente como o 206, mas é em resposta a uma solicitação que NÃO DEVE conter oRange
cabeçalho. Eu acho que o 266 é alto o suficiente para não ter problemas com colisões e isso faz sentido para mim, mas não tenho certeza se isso é considerado tabu ou não.
Eu acho que esse é um problema bastante comum e gostaria de ver isso feito de uma maneira de fato, para que eu ou outra pessoa não esteja reinventando a roda.
Qual é a melhor maneira de expor uma coleção completa via HTTP quando a coleção é grande?
fonte
Range = "Range" ":" ranges-specifier
onde o último em tools.ietf.org/html/rfc2616#section-14.35.1 é descrito meramente como "byte-range-specifier", que deve começar com "bytes-unit", que é definido como a string "bytes "Content-Range
cabeçalho se aplica ao corpo (pode ser usado com solicitação ao fazer upload de arquivos grandes etc, ou como resposta ao fazer o download). ORange
cabeçalho é usado para solicitar um determinado intervalo. Deve-se responder206
quando oRange
cabeçalho foi incluído na solicitação. Caso contrário, a resposta ainda pode incluir umContent-Range
cabeçalho, mas o código de resposta deve estar200
. Na verdade, esse cabeçalho parece ideal para paginação.Respostas:
Minha intuição é que as extensões de intervalo HTTP não foram projetadas para o seu caso de uso e, portanto, você não deve tentar. Uma resposta parcial implica
206
e206
só deve ser enviada se o cliente solicitou.Você pode considerar uma abordagem diferente, como a usada no Atom (em que a representação por design pode ser parcial e é retornada com um status
200
e, potencialmente, com links de paginação). Veja RFC 4287 e RFC 5005 .fonte
items
unidade de alcance, ele retornará uma resposta completa. Estou familiarizado com o Atom, mas essa não é a solução geral para a paginação Rest. Esta não é uma solução para um único caso, mais do que deveria ser a solução geral. Nem todos os documentos / coleções se encaixam no modelo Atom e não há motivo para forçá-lo, a menos que seja necessário.Range
eContent-Range
para fins de paginação.Eu realmente não concordo com alguns de vocês. Estou trabalhando há semanas nesses recursos para o meu serviço REST. O que acabei fazendo é realmente simples. Minha solução só faz sentido para o que as pessoas REST chamam de coleção.
O cliente DEVE incluir um cabeçalho "Range" para indicar de que parte da coleção ele precisa ou estar preparado para lidar com um erro quando a coleção solicitada é muito grande para ser recuperada em uma única viagem de ida e volta.
O servidor envia uma resposta, com o cabeçalho Content-Range especificando qual parte do recurso foi enviada e um cabeçalho ETag para identificar a versão atual da coleção. Normalmente, uso um ETag semelhante ao Facebook {last_modification_timestamp} - {resource_id} e considero que o ETag de uma coleção é o recurso modificado mais recente que ele contém.
Para solicitar uma parte específica de uma coleção, o cliente DEVE usar o cabeçalho "Range" e preencher o cabeçalho "If-Match" com o ETag da coleção obtida de solicitações realizadas anteriormente para adquirir outras partes da mesma coleção. O servidor pode, portanto, verificar se a coleção não foi alterada antes de enviar a parte solicitada. Se existir uma versão mais recente, uma resposta 412 PRECONDITION FAILED será retornada para convidar o cliente a recuperar a coleção do zero. Isso é necessário porque pode significar que alguns recursos podem ter sido adicionados ou removidos antes ou depois da parte solicitada no momento.
Eu uso ETag / If-Match em conjunto com Last-Modified / If-Unmodified-Since para otimizar o cache. Navegadores e proxies podem contar com um ou ambos para seus algoritmos de armazenamento em cache.
Eu acho que um URL deve estar limpo, a menos que inclua uma consulta de pesquisa / filtro. Se você pensar bem, uma pesquisa nada mais é do que uma visão parcial de uma coleção. Em vez dos carros / pesquisa? Q = URLs do tipo BMW, devemos ver mais carros? Fabricante = BMW.
fonte
If-Unmodified-Since
, o que corresponde à variante E-TagIf-Match
, em vez deIf-Modified-Since
. Dito isto, você também pode considerar remover essa restrição, dependendo do seu caso de uso. Digamos que você tenha uma coleção que cresça apenas a partir do topo (como uma coleção de estilos "primeiro mais novo"), o pior que pode acontecer se essa coleção mudar entre solicitações é que um usuário que percorre uma coleção vê as entradas duas vezes. (Que em si também é uma informação útil: Ele informa ao usuário a coleção mudou)413
. Este é um código de erro que significa que o cliente está enviando algo que o servidor se recusa a aceitar devido ao tamanho. Não o contrário! Consulte tools.ietf.org/html/rfc7231#section-6.5.11 (observe que diz carga útil de solicitação . Não carga útil de resposta )!Você ainda pode retornar
Accept-Ranges
eContent-Ranges
com um200
código de resposta. Esses dois cabeçalhos de resposta fornecem informações suficientes para inferir as mesmas informações que um206
código de resposta fornece explicitamente.Eu usaria
Range
para paginação e simplesmente retornaria a200
para uma planícieGET
.Parece 100% RESTful e não torna a navegação mais difícil.
Edit: eu escrevi um post sobre isso: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html
fonte
Se houver mais de uma página de respostas e você não quiser oferecer a coleção inteira de uma só vez, isso significa que existem várias opções?
Em uma solicitação para
/db/questions
, retorne300 Multiple Choices
comLink
cabeçalhos que especificam como chegar a cada página, bem como um objeto JSON ou página HTML com uma lista de URLs.Você teria um
Link
cabeçalho para cada página de resultados (uma sequência vazia significa o URL atual, e o URL é o mesmo para cada página, apenas acessado com intervalos diferentes), e o relacionamento é definido como personalizado de acordo com as próximasLink
especificações . Esse relacionamento explicaria seu costume266
ou sua violação206
. Esses cabeçalhos são sua versão legível por máquina, já que todos os seus exemplos exigem um cliente de entendimento de qualquer maneira.(Se você
2xx
seguir a rota "range", acredito que seu próprio código de retorno, como você o descreveu, seria o melhor comportamento aqui. Espera-se que você faça isso para seus aplicativos e esses códigos de status HTTP são extensíveis. "] e você tem boas razões.)300 Multiple Choices
diz que você também deve fornecer um corpo com uma maneira de escolher o agente do usuário. Se seu cliente está entendendo, ele deve usar osLink
cabeçalhos. Se for um usuário navegando manualmente, talvez uma página HTML com links para um recurso raiz "paginado" especial que possa processar a renderização dessa página específica com base no URL?/humanpage/1/db/questions
ou algo horrível assim?Os comentários no post de Richard Levasseur me lembram uma opção adicional: o
Accept
cabeçalho (seção 14.1). Quando a especificação oEmbed saiu, perguntei-me por que não havia sido feito inteiramente usando HTTP e escrevi uma alternativa usando-as.Mantenha as
300 Multiple Choices
, osLink
cabeçalhos e a página HTML para um HTTP ingênua inicialGET
, mas ao invés de faixas de uso, têm o seu novo relacionamento paginação definir o uso doAccept
cabeçalho. Sua solicitação HTTP subsequente pode se parecer com isso:O
Accept
cabeçalho permite definir um tipo de conteúdo aceitável (seu retorno JSON), além de parâmetros extensíveis para esse tipo (seu número de página). Analisando minhas anotações com base na minha redação escrita oEmbed (não é possível vinculá-la aqui, vou listá-la no meu perfil), você pode ser muito explícito e fornecer uma versão de especificação / relação aqui, caso precise redefinir opage
significado do parâmetro no futuro.fonte
Editar:
Depois de pensar um pouco mais, estou inclinado a concordar que os cabeçalhos de intervalo não são apropriados para paginação. A lógica é que o cabeçalho Range é destinado à resposta do servidor, não aos aplicativos. Se você forneceu 100 megabytes de resultados, mas o servidor (ou cliente) pudesse processar apenas 1 megabyte por vez, bem, é para isso que serve o cabeçalho Range.
Também sou da opinião de que um subconjunto de recursos é seu próprio recurso (semelhante à álgebra relacional.), Por isso merece representação na URL.
Então, basicamente, eu retrocedo minha resposta original (abaixo) sobre o uso de um cabeçalho.
Acho que você respondeu à sua própria pergunta, mais ou menos - retorne 200 ou 206 com o intervalo de conteúdo e, opcionalmente, use um parâmetro de consulta. Eu farejava o agente do usuário e o tipo de conteúdo e, dependendo deles, procurava um parâmetro de consulta. Caso contrário, exija os cabeçalhos do intervalo.
Você tem essencialmente objetivos conflitantes - permita que as pessoas usem o navegador para explorar (o que não permite facilmente cabeçalhos personalizados) ou force as pessoas a usar um cliente especial que pode definir cabeçalhos (o que não permite que eles explorem).
Você pode simplesmente fornecer a eles um cliente especial, dependendo da solicitação - se parecer um navegador simples, envie um pequeno aplicativo ajax que renderize a página e defina os cabeçalhos necessários.
Obviamente, há também o debate sobre se a URL deve conter todo o estado necessário para esse tipo de coisa. A especificação do intervalo usando cabeçalhos pode ser considerada "inanimada" por alguns.
Como um aparte, seria bom se os servidores respondessem com um cabeçalho "Can-Specify: Header1, header2" e os navegadores da Web apresentassem uma interface do usuário para que os usuários pudessem preencher os valores, se desejassem.
fonte
Você pode considerar usar um modelo parecido com o Atom Feed Protocol, pois ele tem um modelo de coleções HTTP sensato e como manipulá-las (onde insana significa WebDAV).
Existe o Atom Publishing Protocol, que define o modelo de coleção e as operações REST, além de você poder usar o RFC 5005 - Feed Paging and Archiving para percorrer grandes coleções.
A mudança do conteúdo do Atom XML para JSON não deve afetar a ideia.
fonte
Acho que o verdadeiro problema aqui é que não há nada na especificação que nos diga como fazer redirecionamentos automáticos quando confrontados com 413 - Entidade solicitada muito grande.
Eu estava lutando com esse mesmo problema recentemente e procurei inspiração no livro RESTful Web Services . Pessoalmente, não acho que o 206 seja apropriado devido ao requisito do cabeçalho. Meus pensamentos também me levaram a 300, mas achei que isso era mais para diferentes tipos de mímica, então procurei o que Richardson e Ruby tinham a dizer sobre o assunto no Apêndice B, página 377. Eles sugerem que o servidor apenas escolha o preferido representação e enviá-lo de volta com um 200, basicamente ignorando a noção de que deveria ser um 300.
Isso também combina com a noção de links para os próximos recursos que temos do átomo. A solução que implementei foi adicionar chaves "next" e "previous" ao mapa json que eu estava enviando de volta e pronto.
Posteriormente, comecei a pensar que talvez a coisa a fazer é enviar um 307 - Redirecionamento temporário para um link que seria algo como / db / questions / 1,25 - que deixa o URI original como o nome do recurso canônico, mas é necessário um recurso subordinado nomeado adequadamente. Esse é um comportamento que eu gostaria de ver em um 413, mas o 307 parece um bom compromisso. Ainda não tentei isso no código ainda. O que seria ainda melhor é o redirecionamento redirecionar para um URL que contém os IDs reais das perguntas mais recentes. Por exemplo, se cada pergunta tiver um ID inteiro, e houver 100 perguntas no sistema e você desejar mostrar as dez mais recentes, as solicitações para / db / questions deverão ser 307 direcionadas para / db / questions / 100,91
Esta é uma pergunta muito boa, obrigado por perguntar. Você confirmou para mim que eu não sou louco por ter passado dias pensando nisso.
fonte
Você pode detectar o
Range
cabeçalho e imitar o Dojo, se estiver presente, e imitar o Atom, se não estiver. Parece-me que isso divide claramente os casos de uso. Se você estiver respondendo a uma consulta REST do seu aplicativo, espera que ele seja formatado com umRange
cabeçalho. Se você estiver respondendo a um navegador casual, se retornar os links de paginação, a ferramenta fornecerá uma maneira fácil de explorar a coleção.fonte
Um dos grandes problemas com os cabeçalhos de intervalo é que muitos proxies corporativos os filtram. Eu aconselho a usar um parâmetro de consulta.
fonte
Com a publicação do rfc723x , as unidades de faixa não registradas vão contra uma recomendação explícita na especificação . Considere rfc7233 (descontinuando rfc2616):
" Novas unidades de intervalo devem ser registradas na IANA " (junto com uma referência a um Registro de unidade de intervalo HTTP ).
fonte
Parece-me que a melhor maneira de fazer isso é incluir o intervalo como parâmetros de consulta. por exemplo, GET / db / questions /? date> mindate e date <maxdate . Em um GET para o / db / questions / sem parâmetros de consulta, retorne 303 com Local: / db / questions /? Query-parameters-to-get-the-default-page . Em seguida, forneça um URL diferente pelo qual quem está consumindo sua API para obter estatísticas sobre a coleção (por exemplo, quais parâmetros de consulta usar se quiser a coleção inteira);
fonte
Embora seja possível usar o cabeçalho Range para esse fim, não acho que tenha sido essa a intenção. Parece ter sido projetado para lidar com conexões inadequadas e também para limitar os dados (para que o cliente possa solicitar parte da solicitação se algo estiver faltando ou se o tamanho for muito grande para processar). Você está invadindo a paginação em algo que provavelmente é usado para outros fins na camada de comunicação. A maneira "adequada" de lidar com a paginação é com os tipos que você retorna. Em vez de retornar o objeto de perguntas, você deve retornar um novo tipo.
Portanto, se as perguntas forem assim:
<questions> <question index=1></question> <question index=2></question> ... </questions>
O novo tipo pode ser algo como isto:
<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>
É claro que você controla seus tipos de mídia, para que você possa transformar suas "páginas" em um formato adequado às suas necessidades. Se você criar algo genérico, poderá ter um único analisador no cliente para manipular a mesma paginação para todos os tipos. Eu acho que isso está mais no espírito da especificação HTTP, em vez de falsificar o parâmetro Range para outra coisa.
fonte