Tentando novamente solicitações HTTP com falha no Angular

A organização do acesso aos dados do servidor é a base de quase qualquer aplicativo de uma página. Todo o conteúdo dinâmico desses aplicativos é baixado do back-end.

Na maioria dos casos, as solicitações HTTP para o servidor funcionam de maneira confiável e retornam o resultado desejado. No entanto, em algumas situações, as solicitações podem falhar.

Imagine como alguém trabalha com seu site por meio de um ponto de acesso em um trem que viaja pelo país a uma velocidade de 200 quilômetros por hora. A conexão de rede nesse cenário pode ser lenta, mas as solicitações do servidor, apesar disso, fazem seu trabalho.

Mas e se o trem entrar no túnel? Há uma alta probabilidade de que a conexão com a Internet seja interrompida e o aplicativo da Web não consiga "alcançar" o servidor. Nesse caso, o usuário precisará recarregar a página do aplicativo depois que o trem deixar o túnel e a conexão com a Internet for restaurada.

Recarregar a página pode afetar o estado atual do aplicativo. Isso significa que o usuário pode, por exemplo, perder os dados inseridos no formulário.

Em vez de simplesmente se reconciliar com o fato de uma determinada solicitação não ter êxito, seria melhor repeti-la várias vezes e mostrar ao usuário uma notificação correspondente. Com essa abordagem, quando o usuário percebe que o aplicativo está tentando lidar com o problema, provavelmente não recarregará a página.



O material, cuja tradução publicamos hoje, é dedicado à análise de várias maneiras de repetir solicitações sem êxito em aplicativos Angular.

Repetir solicitações com falha


Vamos reproduzir uma situação que um usuário que trabalha na Internet a partir de um trem pode encontrar. Criaremos um back-end que processa a solicitação incorretamente durante as três primeiras tentativas de acesso, retornando dados apenas da quarta tentativa.
Geralmente, usando o Angular, criamos um serviço, conectamos o HttpClient e o usamos para obter dados do back-end.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      catchError(() => {        //           return EMPTY;      })    );  } } 

Não há nada de especial aqui. HttpClient módulo Angular HttpClient e executamos uma solicitação GET simples. Se a solicitação retornar um erro, executamos algum código para processá-lo e retornamos um Observable (objeto observável) vazio para informar sobre o que iniciou a solicitação. Este código, por assim dizer, diz: "Houve um erro, mas está tudo em ordem, eu posso lidar com isso".

A maioria dos aplicativos executa solicitações HTTP dessa maneira. No código acima, a solicitação é executada apenas uma vez. Depois disso, ele retorna os dados recebidos do servidor ou não tem êxito.

Como repetir a solicitação se o terminal /greet indisponível ou retorna um erro? Talvez exista uma declaração RxJS adequada? Claro que existe. O RxJS tem operadores para tudo.

A primeira coisa que pode surgir nessa situação é a retry . Vejamos sua definição: “Retorna um Observable que reproduz o Observable original, exceto por error . Se o Observable original chamar error , esse método, em vez de propagar o erro, se inscreverá novamente no Observable original.

O número máximo de novas assinaturas é limitado à count (este é o parâmetro numérico passado ao método). "

A retry muito semelhante ao que precisamos. Então, vamos incorporá-lo em nossa cadeia.

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      retry(3),      catchError(() => {        //           return EMPTY;      }),      shareReplay()    );  } } 

Utilizamos com sucesso o operador de retry . Vamos ver como isso afetou o comportamento da solicitação HTTP executada no aplicativo experimental. Aqui está um arquivo GIF grande que mostra a tela deste aplicativo e a guia Rede das ferramentas de desenvolvedor do navegador. Você encontrará várias outras demonstrações aqui.

Nossa aplicação é extremamente simples. Apenas faz uma solicitação HTTP quando o botão PING THE SERVER é clicado.

Como já mencionado, o back-end retorna um erro ao executar as três primeiras tentativas de executar uma solicitação e, quando uma quarta solicitação chega, retorna uma resposta normal.

Na guia de ferramentas do desenvolvedor de rede, você pode ver que a retry resolve a tarefa atribuída a ela e repete a execução da solicitação com falha três vezes. A última tentativa foi bem-sucedida, o aplicativo recebe uma resposta, uma mensagem correspondente aparece na página.

Tudo isso é muito bom. Agora o aplicativo pode repetir solicitações com falha.

No entanto, este exemplo ainda pode ser aprimorado. Observe que agora solicitações repetidas são executadas imediatamente após a execução de solicitações sem êxito. Esse comportamento do sistema não trará muitos benefícios em nossa situação - quando o trem entrar no túnel e a conexão com a Internet for perdida por um tempo.

Nova tentativa atrasada de solicitações com falha


O trem que entrou no túnel não sai instantaneamente. Ele passa algum tempo lá. Portanto, precisamos "esticar" o período durante o qual executamos solicitações repetidas ao servidor. Você pode fazer isso adiando novas tentativas.

Para fazer isso, precisamos controlar melhor o processo de execução de solicitações repetidas. Precisamos ser capazes de tomar decisões sobre quando exatamente repetir solicitações. Isso significa que os recursos do operador de retry não são mais suficientes para nós. Portanto, voltamos à documentação do RxJS.

A documentação contém uma descrição da retryWhen , que parece nos adequar. Na documentação, é descrito da seguinte maneira: “Retorna um Observable que reproduz o Observable original, com exceção do error . Se o Observable original chamar error , esse método lançará Throwable, que causou o erro, o Observable retornou do notifier . Se este Observável chamar complete ou com error , esse método chamará complete ou com error na assinatura filho. Caso contrário, esse método se inscreverá novamente no Observável original ".

Sim, a definição não é simples. Vamos descrever o mesmo em um idioma mais acessível.

A retryWhen aceita um retorno de chamada que retorna um Observable. O Observável retornado decide como o operador retryWhen se comportará com base em algumas regras. Ou seja, é assim que o operador retryWhen :

  • Ele para de funcionar e gera um erro se o Observável retornado gerar um erro.
  • Ele sai se o Observável retornado concluir os relatórios.
  • Em outros casos, quando o Observable retorna com êxito, repete a execução do Observable original

Um retorno de chamada é chamado apenas quando o Observable original gera um erro pela primeira vez.

Agora, podemos usar esse conhecimento para criar um mecanismo de nova tentativa atrasada para uma solicitação com falha usando a retryWhen RxJS retryWhen .

 retryWhen((errors: Observable<any>) => errors.pipe(    delay(delayMs),    mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry))    )) ) 

Se o Observable original, que é a nossa solicitação HTTP, retornar um erro, a instrução retryWhen será retryWhen . No retorno de chamada, temos acesso ao erro que causou a falha. Adiamos errors , reduzimos o número de tentativas e retornamos um novo Observable que gera um erro.

Com base nas regras da retryWhen , este Observable, uma vez que retryWhen , retryWhen solicitação. Se a repetição não for bem-sucedida várias vezes e o valor da variável de retries diminuir para 0, encerraremos a tarefa com um erro que ocorreu durante a execução da solicitação.

Ótimo! Aparentemente, podemos pegar o código acima e substituir o operador de retry em nossa cadeia por ele. Mas aqui desaceleramos um pouco.

Como retries com as retries variáveis? Esta variável contém o estado atual do sistema com falha na nova tentativa de solicitação. Onde ela é anunciada? Quando a condição é redefinida? O estado precisa ser gerenciado dentro do fluxo, não fora dele.

Crie sua própria declaração delayedRetry


Podemos resolver o problema do gerenciamento de estado e melhorar a legibilidade do código, escrevendo o código acima como um operador RxJS separado.

Existem diferentes maneiras de criar seus próprios operadores RxJS. Qual método usar depende de como o operador específico está estruturado.

Nosso operador é baseado nos operadores RxJS existentes. Como resultado, podemos usar a maneira mais simples de criar nossos próprios operadores. No nosso caso, o operador RxJs é apenas uma função com a seguinte assinatura:

 const customOperator = (src: Observable<A>) => Observable<B> 

Esta declaração pega o Observable original e retorna outro Observable.

Como nosso operador permite que o usuário especifique com que freqüência as solicitações repetidas devem ser executadas e quantas vezes precisam ser executadas, precisamos envolver a declaração da função acima em uma função de fábrica, que leva delayMs (delay entre maxRetry ) e maxRetry ( número máximo de repetições).

 const customOperator = (delayMs: number, maxRetry: number) => {   return (src: Observable<A>) => Observable<B> } 

Se você deseja criar um operador que não seja baseado em operadores existentes, preste atenção ao tratamento de erros e assinaturas. Além disso, você precisará estender a classe Observable e implementar a função de lift .

Se você estiver interessado, dê uma olhada aqui .

Portanto, com base nos trechos de código acima, vamos escrever nosso próprio operador RxJs.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        delay(delayMs),        mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry))        ))      )    ); } 

Ótimo. Agora podemos importar esse operador para o código do cliente. Nós o usaremos ao executar uma solicitação HTTP.

 return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(        delayedRetry(1000, 3),        catchError(error => {            console.log(error);            //               return EMPTY;        }),        shareReplay()    ); 

Colocamos o operador delayedRetry na cadeia e passamos os números 1000 e 3. Como parâmetros O primeiro parâmetro define o atraso em milissegundos entre tentativas de fazer solicitações repetidas. O segundo parâmetro determina o número máximo de solicitações repetidas.

Reinicie o aplicativo e veja como o novo operador funciona.

Após analisar o comportamento do programa usando as ferramentas do desenvolvedor do navegador, podemos ver que a execução de tentativas repetidas de executar a solicitação está atrasada por um segundo. Após receber a resposta correta para a solicitação, uma mensagem correspondente aparecerá na janela do aplicativo.

Adiamento exponencial da solicitação


Vamos desenvolver a idéia de nova tentativa atrasada de solicitações com falha. Anteriormente, sempre atrasávamos a execução de cada uma das solicitações repetidas ao mesmo tempo.

Aqui falamos sobre como aumentar o atraso após cada tentativa. A primeira tentativa de repetir a solicitação é feita após um segundo, a segunda após dois segundos e a terceira após três.

Crie uma nova instrução, retryWithBackoff , que implementa esse comportamento.

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        mergeMap(error => {            if (retries-- > 0) {              const backoffTime = delayMs + (maxRetry - retries) * backoffMs;              return of(error).pipe(delay(backoffTime));            }            return throwError(getErrorMessage(maxRetry));          }        )))); } 

Se você usar esse operador no aplicativo e testá-lo, poderá ver como o atraso na execução da solicitação repetida aumenta após cada nova tentativa.

Após cada tentativa, aguardamos um certo tempo, repetimos a solicitação e aumentamos o tempo de espera. Aqui, como de costume, depois que o servidor retorna a resposta correta para a solicitação, exibimos uma mensagem na janela do aplicativo.

Sumário


A repetição de solicitações HTTP com falha torna os aplicativos mais estáveis. Isso é especialmente significativo ao executar consultas muito importantes, sem os dados obtidos através dos quais o aplicativo não pode funcionar normalmente. Por exemplo, podem ser dados de configuração que contêm os endereços dos servidores com os quais o aplicativo precisa interagir.

Na maioria dos cenários, a instrução de repetição de RxJs não retry suficiente para fornecer um sistema de repetição confiável para solicitações com falha. A retryWhen fornece ao desenvolvedor um nível mais alto de controle sobre solicitações repetidas. Permite configurar o intervalo para solicitações repetidas. Devido às capacidades desse operador, é possível implementar um esquema de repetição atrasada ou repetições exponencialmente atrasadas.

Ao implementar padrões de comportamento adequados para reutilização em cadeias RxJS, é recomendável que eles sejam formatados como novos operadores. Aqui está o repositório a partir do qual o código foi usado neste artigo.

Caros leitores! Como você resolve o problema de tentar novamente solicitações HTTP com falha?

Source: https://habr.com/ru/post/pt459302/


All Articles