Como uma API REST deve manipular solicitações PUT para recursos parcialmente modificáveis?


46

Suponha que uma API REST, em resposta a uma GETsolicitação HTTP , retorne alguns dados adicionais em um subobjeto owner:

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'Programmer'
  }
}

Claramente, não queremos que ninguém possa PUTvoltar atrás.

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'CEO'
  }
}

e ter esse sucesso. De fato, provavelmente nem vamos implementar uma maneira de que isso possa ser bem-sucedido, nesse caso.

Mas essa pergunta não é apenas sobre subobjetos: o que, em geral, deve ser feito com dados que não devem ser modificáveis ​​em uma solicitação PUT?

Deveria estar ausente a solicitação PUT?

Deve ser descartado silenciosamente?

Ele deve ser verificado e se diferir do valor antigo desse atributo, retorne um código de erro HTTP na resposta?

Ou devemos usar patches JSON RFC 6902 em vez de enviar o JSON inteiro?


2
Tudo isso funcionaria. Eu acho que depende de suas necessidades.
Robert Harvey

Eu diria que esse princípio de menor surpresa indicaria que deveria estar faltando na solicitação de PUT. Se não for possível, verifique e se é diferente e retorne com o código de erro. O descarte silencioso é pior (o envio do usuário espera que ele mude e você diz "200 OK").
Maciej Piechotka

2
@MaciejPiechotka o problema é que você não consegue usar o mesmo modelo no put como no insert ou get etc, eu preferiria que o mesmo modelo fosse usado e que existissem regras de autorização de campo simples, se eles inserirem um valor para um campo que não deve mudar, eles voltarem a 403 Proibido, e se, mais tarde, a autorização é configurado para permitir que eles obtêm um 401 não autorizado, se eles não estão autorizados
Jimmy Hoffa

@ JimmyHoffa: Por modelo, você quer dizer o formato dos dados (pois pode ser possível reutilizar o modelo na estrutura MVC Rest, dependendo da escolha, se houver algum usado - o OP não mencionou nenhum)? Eu aceitaria a descoberta se não fosse uma restrição pela estrutura e o erro inicial fosse um pouco mais detectável / fácil de implementar do que verificando alterações (ok - não devo tocar no campo XYZ). De qualquer forma, o descarte é pior.
Maciej Piechotka

Respostas:


46

Não existe uma regra, na especificação do W3C ou nas regras não oficiais do REST, que diz que um PUTdeve usar o mesmo esquema / modelo que o correspondente GET.

É bom se eles são semelhantes , mas não é incomum PUTfazer as coisas de maneira um pouco diferente. Por exemplo, vi muitas APIs que incluem algum tipo de ID no conteúdo retornado por a GET, por conveniência. Mas com a PUT, esse ID é determinado exclusivamente pelo URI e não tem significado no conteúdo. Qualquer ID encontrado no corpo será ignorado silenciosamente.

O REST e a web em geral estão fortemente ligados ao Princípio da Robustez : "Seja conservador no que faz [envie], seja liberal no que aceita". Se você concorda filosoficamente com isso, a solução é óbvia: ignore todos os dados inválidos nas PUTsolicitações. Isso se aplica aos dados imutáveis, como no seu exemplo, e às bobagens reais, por exemplo, campos desconhecidos.

PATCHé potencialmente outra opção, mas você não deve implementá- PATCHlo, a menos que realmente ofereça suporte a atualizações parciais. PATCHsignifica apenas atualizar os atributos específicos que incluo no conteúdo ; isso não significa substituir a entidade inteira, mas excluir alguns campos específicos . Na verdade, o que você está falando não é uma atualização parcial, é uma atualização completa, idempotente e tudo mais, é apenas que parte do recurso é somente leitura.

Uma coisa boa a fazer se você escolher essa opção seria enviar de volta um 200 (OK) com a entidade atualizada real na resposta, para que os clientes possam ver claramente que os campos somente leitura não foram atualizados.

Certamente, algumas pessoas pensam o contrário - que deve ser um erro tentar atualizar uma parte somente leitura de um recurso. Há alguma justificativa para isso, principalmente com base em que você definitivamente retornaria um erro se todo o recurso fosse somente leitura e o usuário tentasse atualizá-lo. Definitivamente, isso contraria o princípio da robustez, mas você pode considerar que é mais "auto-documentável" para os usuários da sua API.

Existem duas convenções para isso, as quais correspondem às suas idéias originais, mas eu as expandirei. O primeiro é proibir que os campos somente leitura apareçam no conteúdo e retornar um HTTP 400 (Solicitação incorreta), se houver. APIs desse tipo também devem retornar um HTTP 400 se houver outros campos não reconhecidos / inutilizáveis. O segundo é exigir que os campos somente leitura sejam idênticos ao conteúdo atual e retornar um 409 (Conflito) se os valores não corresponderem.

Eu realmente não gosto da verificação de igualdade com 409 porque, invariavelmente, exige que o cliente faça um GETpara recuperar os dados atuais antes de poder fazer um PUT. Isso não é legal e provavelmente levará a um desempenho ruim, para alguém, em algum lugar. Eu também realmente não gosto do 403 (Proibido) por isso, pois implica que todo o recurso está protegido, não apenas uma parte dele. Portanto, minha opinião é que, se você absolutamente precisar validar, em vez de seguir o princípio de robustez, valide todas as suas solicitações e retorne 400 para qualquer um que tenha campos extras ou não graváveis.

Verifique se o seu 400/409 / Whatever inclui informações sobre qual é o problema específico e como corrigi-lo.

Ambas as abordagens são válidas, mas eu prefiro a primeira, de acordo com o princípio da robustez. Se você já experimentou trabalhar com uma grande API REST, apreciará o valor da compatibilidade com versões anteriores. Se você decidir remover um campo existente ou torná-lo somente leitura, será uma alteração compatível com versões anteriores se o servidor simplesmente ignorar esses campos e os clientes antigos ainda funcionarem. No entanto, se você fizer uma validação estrita do conteúdo, ele não será mais compatível com versões anteriores e os clientes antigos deixarão de funcionar. O primeiro geralmente significa menos trabalho para o mantenedor de uma API e seus clientes.


1
Boa resposta e votada. No entanto, não tenho certeza se concordo com isso: "Se você decidir remover um campo existente ou torná-lo somente leitura, será uma alteração compatível com versões anteriores se o servidor simplesmente ignorar esses campos e os clientes antigos ainda funcionarão. " Se o cliente dependesse desse campo removido / recém-somente leitura, isso ainda não afetaria o comportamento geral do aplicativo? No caso de remover campos, eu diria que provavelmente é melhor gerar explicitamente um erro em vez de ignorar os dados; caso contrário, o cliente não tem idéia de que sua atualização que estava funcionando anteriormente está falhando agora.
Rinogo 30/08/16

Esta resposta está errada. por 2 razões da RFC2616: 1. (seção 9.1.2) A PUT deve ser independente. Coloque muitas vezes e produzirá o mesmo resultado que colocar apenas uma vez. 2. O a chegar a um recurso deve retornar a entidade posta se nenhum outro pedido for feito para alterar o recurso #
315

1
E se você fizer a verificação de igualdade apenas se o valor imutável foi enviado na solicitação. Eu acho que isso lhe dá o melhor dos dois mundos; você não força os clientes a fazer um GET e ainda os notifica que algo está errado se eles enviarem um valor inválido para um imutável.
Ahmad Abdelghany

Obrigado, a comparação aprofundada que você fez nos últimos parágrafos provenientes da experiência é exatamente o que eu estava procurando.
dhill 14/02

9

Potência Idem

Após o RFC, um PUT teria que entregar um objeto completo ao recurso. A principal razão disso é que o PUT deve ser idempotente. Isso significa que uma solicitação repetida deve ser avaliada para o mesmo resultado no servidor.

Se você permitir atualizações parciais, ela não poderá mais ser idem-potente. Se você tem dois clientes. Cliente A e B, o seguinte cenário pode evoluir:

O cliente A obtém uma imagem das imagens de recursos. Isso contém uma descrição da imagem, que ainda é válida. O cliente B coloca uma nova imagem e atualiza a descrição de acordo. A imagem mudou. O cliente A vê, ele não precisa alterar a descrição, porque é como ele deseja e colocar apenas a imagem.

Isso levará a uma inconsistência, a imagem tem os metadados incorretos anexados!

Ainda mais irritante é que qualquer intermediário pode repetir a solicitação. Caso decida de alguma forma, o PUT falhou.

O significado de PUT não pode ser alterado (embora você possa usá-lo mal).

Outras opções

Felizmente, há uma outra opção, este é o PATCH. PATCH é um método que permite atualizar parcialmente uma estrutura. Você pode simplesmente enviar uma estrutura parcial. Para aplicativos simples, isso é bom. Não é garantido que este método seja idem potente. O cliente deve enviar uma solicitação no seguinte formato:

PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 20
{fielda: 1, fieldc: 2}

E o servidor pode responder novamente com 204 (Sem conteúdo) para sinalizar o sucesso. Por erro, você não pode atualizar uma parte da estrutura. O método PATCH é atômico.

A desvantagem desse método é que nem todos os navegadores suportam isso, mas essa é a opção mais natural em um serviço REST.

Exemplo de solicitação de patch: http://tools.ietf.org/html/rfc5789#section-2.1

Correção de Json

A opção json parece ser bastante abrangente e uma opção interessante. Mas pode ser difícil de implementar para terceiros. Você precisa decidir se sua base de usuários pode lidar com isso.

Também é um pouco complicado, porque você precisa criar um pequeno intérprete que converta os comandos em uma estrutura parcial, que você usará para atualizar seu modelo. Esse intérprete também deve verificar se os comandos fornecidos fazem sentido. Alguns comandos se cancelam. (escreva fielda, exclua fielda). Acho que você deseja denunciar isso ao cliente para limitar o tempo de depuração do lado dele.

Mas se você tiver tempo, essa é uma solução realmente elegante. Você ainda deve validar os campos, é claro. Você pode combinar isso com o método PATCH para permanecer no modelo REST. Mas acho que o POST seria aceitável aqui.

Indo mal

Se você decidir optar pela opção PUT, o que é um pouco arriscado. Então você deve pelo menos não descartar o erro. O usuário tem uma certa expectativa (os dados serão atualizados) e, se você quebrar isso, não dará um bom tempo a alguns desenvolvedores.

Você pode optar por sinalizar de volta: 409 Conflito ou 403 Proibido. Depende de como você olha o processo de atualização. Se você o vir como um conjunto de regras (centralizado no sistema), o conflito será mais agradável. Algo como, esses campos não são atualizáveis. (Em conflito com as regras). Se você o vir como um problema de autorização (centrado no usuário), deverá retornar proibido. Com: você não está autorizado a alterar esses campos.

Você ainda deve forçar os usuários a enviar todos os campos modificáveis.

Uma opção razoável para impor isso é configurá-lo como um sub-recurso, que oferece apenas os dados modificáveis.

Opinião pessoal

Pessoalmente, eu iria (se você não precisar trabalhar com navegadores) pelo modelo PATCH simples e depois o estenderia com um processador de patch JSON. Isso pode ser feito diferenciando-se nos tipos mimetizados: O tipo mime do patch json:

application / json-patch

E json: application / json-patch

facilita a implementação em duas fases.


3
Seu exemplo de idempotência não faz sentido. Ou você muda a descrição ou não. De qualquer maneira, você obterá o mesmo resultado sempre.
21713 Robert Harvey

1
Você está certo, acho que é hora de ir para a cama. Não consigo editar. É mais um exemplo sobre o racional de enviar todos os dados em uma solicitação PUT. Obrigado pelo ponteiro.
Edgar Klerks

Eu sei que isso foi há 3 anos ... mas você sabe onde na RFC posso encontrar mais informações sobre "O PUT teria que entregar um objeto completo ao recurso". Eu já vi isso mencionado em outro lugar, mas gostaria de ver como é definido nas especificações.
CSharper

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.