Implementar soluções RESTful nem sempre é tarefa trivial. Neste apêndice, apresentamos algumas soluções para desafios comuns.
Suportando operações com longa duração
HTTP é um protocolo síncrono e stateless. Sempre que uma requisição é submetida a um “componente servidor”, é normal que o “componente cliente” espere por uma resposta indicando falha ou sucesso. Entretanto, nem sempre é possível executar todo o processamento em tempo razoável.
Operações de longa duração iniciadas por HTTP POST
Problema
Eventualmente, operações iniciadas com verbos HTTP POST demoram para serem completadas. Logo, se implementadas ingenuamente, “seguram” o componentes “cliente” por muito tempo, eventualmente com timeout.
Solução
Ao executar operações de longa duração, iniciadas por verbos HTTP POST, o componente “servidor” deve retornar rapidamente uma response marcada com código HTTP 202 (Accepted) e com uma representação de um recurso específico para indicar o andamento.
HTTP/1.1 202 Accepted Content-Type: application/json; charset=UTF-8 Content-Location: /tasks/1 Date: ... { status: "accepted", message: "Your request has been accepted for processing.", ping-after: ... links: [ { rel: "self", href: "/tasks/1" } ] }
É importante que este recurso possua, além de um indicador de status uma “previsão” de quanto tempo a operação deverá consumir.
Solicitações HTTP GET futuras para o recurso indicado na URL de acompanhamento devem ser ajustadas conforme estado de processamento:
- se o processamento ainda não tiver sido completado, o recurso deve indicar status “still Processing”, retornando 200 (OK). Além disso, é recomendável ajustar o caching considerando tempo de processamento, aliviando pressão sobre o componente “servidor”
- se o processamento foi completado com êxito, retornar 303 (Ser Other) com Location indicando URL do recurso gerado.
- se o processamento não foi completado com êxito, retornar 200 (OK) com documento de status indicando a falha que ocorreu.
HTTP não foi pensado para ser assíncrono
Um detalhe que pode chamar a atenção na solução proposta é o retorno de 200 (OK), para recuperar o recurso de estado quando há falha no processamento. O motivo para isso é que a recuperação do recurso com o status, em si, não falhou.
Operações de longa duração iniciadas por HTTP POST e DELETE
Problema
Eventualmente, operações iniciadas com verbos HTTP PUT e DELETE demoram para serem completadas. Logo, se implementadas ingenuamente, “seguram” o componentes “cliente” por muito tempo, eventualmente com timeout.
Solução
Ao executar operações de longa duração, iniciadas por verbos HTTP DELETE, o componente “servidor” deve retornar rapidamente uma response marcada com código HTTP 202 (Accepted) e com uma representação de um recurso específico para indicar o andamento, de forma quase idêntica a abordagem para suportar operações HTTP POST – a mudança é não fazer redirecionamento ao concluir a operação (opcionalmente, para operações PUT)
Requisições condicionais (caching)
Operações HTTP GET não suportadas por caching podem comprometer a performance de uma API em produção. A utilização inadequada de caching pode causar problemas sérios de consistência em operações PUT, POST e DELETE (não seguras).
Toda vez que um componente “cliente” utilizar mecanismos de caching local, afim de reduzir a quantidade de requisições custosas para um componente “servidor” para melhorar a performance, deverá armazenar, também, no cache as informações Last-Modified e ETag.
Gestão de concorrência
Assuma o seguinte fluxo:
- Dois usuários, em duas máquinas separadas, acessam o “cadastro” de um mesmo cliente (HTTP GET)
- Os dois usuários alteram em “campos” diferentes
- Um dos usuários, submete atualização dos dados do cliente (por exemplo, usando HTTP PUT), mudando uma representação do recurso, pedindo atualização de estado no componente “servidor”
- O segundo usuário, logo depois, também submete atualização de dados.
Como impedir, no fluxo, que o segundo usuário não “mate” as alterações do primeiro? Este problema é reconhecido, em TI, como gestão de concorrência. Há duas abordagens comuns:
- concorrência pessimista – onde um usuário, ao obter dados para alteração, “bloqueia” o acesso ao recurso para outros fluxos de alteração concorrentes até que as modificações estejam efetivadas.
- concorrência otimista – onde sempre que um componente “cliente” recupera o estado de um recurso, obtém junto um token de versionamento (que é modificado sempre que o recurso tem estado atualizado no componente “servidor”). Dessa forma, sempre que uma tentativa de atualização é realizada, o token de versionamento que foi obtido no GET é enviado junto no request (operações PUT, POST ou DELETE), sendo que, se já não for mais o token vigente, a operação falha.
Por causa da restrição de que serviços REST devem ser stateless, apenas a abordagem otimista é considerada válida.
Estabelecendo tokens de versionamento
Em HTTP há duas abordagens comuns para associação de tokens de versionamento de recursos:
- Atributo de cabeçalho Last-Modified – explicitando quando aconteceu a última modificação (em horário do servidor)
- Atributo de cabeçalho ETag – com uma “chave de conteúdo”, geralmente hash ou versão.
Essas tokens podem ser determinadas seguindo uma das seguintes abordagens:
- Caso seja possível armazenar, junto as informações de recurso, a data da última modificação ou um número de versão (por exemplo, em uma coluna no banco de dados), estes valores poderão ser utilizados, também, para Last-Modified e ETag.
- Se o recurso não for grande ou custoso de ser obtido, poderá ter uma hash (por exemplo MD5) computada on-the-fly sempre que necessário.
- Se o recurso for grande ou custoso de ser obtido, a hash poderá ser gerada sempre que o estado do recurso for modificado e armazenada
Operações com GET ou HEAD condicional
Requisições para a URL de um recurso armazenado no cache devem conter os valores de If-Modified-Since e If-None-Match ajustados com, respectivamente, os valores de Last-Modified e ETag da última requisição.
Sempre que um componente “servidor” receber uma requisição HTTP GET com valores em If-Modified-Since e If-None-Match deverá compará-los com os valores vigentes e, se permanecerem iguais, retornar 304 (Not Modified), sinalizando para o “cliente” que os valores em cache permanecem válidos. Caso contrário, uma nova representação do recurso deverá ser retornada com o código 200 (Ok)
Operações com PUT ou DELETE condicional
Atualizações em recursos em que é necessário manter gestão de concorrência otimista devem conter os valores de If-Unmodified-Since e If-Match ajustados com, respectivamente, os valores de Last-Modified e ETag da última requisição.
Requisições PUT ou DELETE para recursos que tem gestão de concorrência otimista:
- que não tenham valores definidos para If-Unmodified-Since e If-Match, devem ser “negadas” pelo servidor retornando 403 (Forbidden).
- que tenham tokens de versionamento desatualizados, devem ser “negadas” pelo servidor retornando 412 (Pre condition failed)
- que tenham tokens de versionamento atualizados, devem ser “aceitas” pelo servidor, retornando 200 (OK) ou 204 (No Content), junto com, para requisições PUT, atualizações para Last-Modified e ETag
Sempre que um componente “servidor” receber uma requisição HTTP GET com valores em If-Modified-Since e If-None-Match deverá compará-los com os valores vigentes e, se permanecerem iguais, retornar 304 (Not Modified), sinalizando para o “cliente” que os valores em cache permanecem válidos. Caso contrário, uma nova representação do recurso deverá ser retornada com o código 200 (Ok).