Галоўная / Артыкулы

Проектаванне для няверных запыткаў у Angular: статусы, інтерцэпторы і павтарэння

Выучыце, як класіфікаціяваць неудачы HTTP у Angular, адчыстыце стан завантажэння, централізавайце обработку ў інтэраптарах, адбывайце павторныя спробы безпечна і паказвайце прыемлівыя для корыстнікаў паведамленні.

Выдавец: REACTAPP.TOP9 хв чытання2160 слоў

Крыніцы: **Handling API Failures Gracefully in Angular: Practical Error Handling Patterns Every Frontend Developer Should Know**

Большая частка компанентаў Angular пісаныя для стандартнага развітку справ: 200 OK, даныя ў атрыбуте, шаблон адрасаваны. У працэўным режыме з’являюцца прычыны, якія парадузуюць з’ўязакі, сессіі выканчваюцца, бэкенды зламваюцца, а корыстувальнікі двойная клікаюць на кнопкі адправы. Наведаныя нижэй патерны дазволяюць аплікацыі Angular з’явіцца пра гэтыя неудачы, поясніць іх корыстувальніку, вярнуцца да нормальнага стану там, дзе гэта можліва, і запростаць разработчыкам достатню інфармацыю для выліковання прычыны.

Цена ігноравання функцыі адпаведзення на адказ

Тыповы запит да компанента падпішываеся на адказ, прызначае яго і больш нічога не робіць.

this.http.get('/api/accounts').subscribe({
  next: (response) => {
    this.accounts = response;
  }
});

Якщо сервер адпавядае кодам 500, нічога візуальна не выканчваецца. Старонка застаецца порожней або спінер продовжвае працаваць, а ўсё, што застаецца, — гэта запіс у консолі, якую корыстувальнік ніколі не ачынае.

Мінімальны спосаб прылагоджэння — це наявнасць обробніка error, який фіксуе неудачу і зберагае чытальныя паведамленні для шаблона.

this.http.get('/api/accounts').subscribe({
  next: (response) => {
    this.accounts = response;
  },
  error: (error) => {
    console.error('Failed to load accounts', error);
    this.errorMessage = 'Unable to load accounts. Please try again.';
  }
});

Эта методыка працюе для адзінаго экрана. У разы ў дзесятках экранаў копіюванне прыемніка ў всіх месцах прагчынае неоднаковымі запаведзямі, забутымі крайнімі случаямі і дуплікацыяй коду, таму так важлівы перадаюцыяся патэрны.

Не всі адказы пра бядах значаць тое ж

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

400 Bad Request: даныя введання некоректныя

Фронтэнд апаслаў некоректныя даны.

400 Bad Request

Паўзначальнік зазвычай можа сам вырашыць проблему, напрыклад, пасправіўшы поле формы.

401 Unauthorized: сесія не існуе або выйшла з часу

Няма аутантыкацыйных дакументаў, часта таму, што токен доступу выйшаў з часу.

401 Unauthorized

Звычайная рэакцыя — пераправленне на вход або спачатку паўнаваганенне токена.

403 Forbidden: аутантыфікованы, але не мае права

Сервер знае паўзначальніка, але адмовляе ў выкананні дзеяння.

403 Forbidden

Это стандартная ситуация з контралем даступу на адміністратыўны рэжым. Перадача пользователя на сторонню панель аблогіну не дапаможа; ёму трэба з’ясавіць, што у яго няма прав.

404 Не знайдзена: рэсурс не існуе

Этот элемент быў выдалены або URL ўстарэў.

404 Not Found

500 Внутранія памылка сервера: бэкенд зламаны

Што-та паспяліло на серверы.

500 Internal Server Error

Пользователь не можа гэта паспрабаваць выправіць; трэба падаць чыстыя інструкцыі і можласць спробаваць зноў пазней.

Памылкі сеті: запит так і не дасягнуў сервера

Іноды запит узагалі не доходзіць да сервера.

Internet connection lost
Server unavailable
DNS failure
Request timeout

Angular фіксуе гэтыя ситуацыі як HttpErrorResponse з status 0, чым адзначае іх ад справжніх адпаведзей сервера. Гэтая катагорыя вядомае все, што будзе далей: чы трэба прабавіць зноў, перенаправіць і што падаць корыстніку.

Моделюйце запит як сукупнас станоў

Адстэйкаванне толькі ад успеху — частая памылка. Спрыяйце запіту як малайшаму машынам станоў: ён пачынае завантажэнне і заканчваецца або успехам, або памылкай.

Loading
   ↓
Success

or

Loading
   ↓
Error

У компаненте гэта значыць явныя флагі для завантажэння і паведамлення пра памылку, якія апдэйтуюцца на кожнай гілцы.

loading = false;
errorMessage = '';
loadAccounts() {
  this.loading = true;
  this.errorMessage = '';
  this.http.get('/api/accounts').subscribe({
    next: (data) => {
      this.accounts = data;
      this.loading = false;
    },
    error: () => {
      this.errorMessage =
        'Unable to load accounts. Please try again.';
      this.loading = false;
    }
  });
}

Ключовы момент — абнуленне loading у функцыі-вярненні памылкі; без гэтага кожная няудача заставляе спінер працаваць без канца. Абнуленне errorMessage на пачатку таксама запобегае тым, каб старая памылка засталася разам з новымі дадзеннямі.

Дазвольце функциі finalize() абрасаваць флаг завантажэння

Абнуленне флага en двух месцах ўскладняе карыстоўванне. Функцыя RxJS finalize() дае можлівасць абрасаванню ў адным месцы.

import { finalize } from 'rxjs';
loadAccounts() {
  this.loading = true;
  this.http.get('/api/accounts')
    .pipe(
      finalize(() => {
        this.loading = false;
      })
    )
    .subscribe({
      next: (data) => {
        this.accounts = data;
      },
      error: () => {
        this.errorMessage =
          'Unable to load accounts.';
      }
    });
}

Функцыя-вызов finalize() выконваецца, калі об’ект, які можна спостерагаць, завершыў свою роботу або выклікаў памылку, а таксама калі падпіска ўсунутая раней. Гэта робіць яе ідеальной для сховання індыкатора завантажэння, і лініі this.loading = false, якія павтарыліся, зникаюць.

Централізаванне спяльнага поведэння ў інтерцептары HTTP

Заканчэнне терміну дзейснасці сесіі або загальныя перывы у роботе службы трэба обрабоцваць аднакова на кожной сторонцы, таму ўпроваджэнне такой логіки для кожнага компонента є повтаральным і нестабільным. Інтерцептары HTTP бачаюць кожны запит HttpClient, таму яны є правым месцам для застосавання правіл на всю аплікацыю. Цей прыклад выкарыстоўвае функцыйнальны стыл HttpInterceptorFn і робіць розгалужэння на паўзе запиту.

export const errorInterceptor: HttpInterceptorFn =
  (req, next) => {
    return next(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          console.log('Session expired');
        }
        if (error.status === 403) {
          console.log('Access denied');
        }
        if (error.status >= 500) {
          console.log('Server error');
        }
        return throwError(() => error);
      })
    );
  };

Выклікі для логавання ўжоўташневаныя як месца для перенаправлення, паведамлення аб проблеме чы ўведчэння падазроўных дзейнасцей. Што ўжо важлівасцю, інтэрсептар заканчваецца кодам throwError(() => error), таму проблема все равно доходзіць да компонента, які ўжоўвае цей код. Інтэрсептар адпавядае за спільныя для кантэкста задачы, а компонент — за тое, што розумее толькі ён; такая сепарацыя дапамагае падтрымваць код у працездатны стані. Функцыональныя інтэрсептары рэгіструюцца за дапамогою provideHttpClient(withInterceptors([...])).

Перакладаць тэхнічныя проблемы на зрозумелы для людзей тэкст

Бэкенды не завжды вяртаюць дружелюбны тэкст:

{
  "message": "NullPointerException at AccountService.java:142"
}

Это дапамагае адпаведальным за сервіс, але для кліента такі тэкст вызывае плутанне і адкрывае деталі рэалізацыі. Натоўпіце замест таго ўсё, што напісана для корыстніка.

We couldn't load your accounts right now.
Please try again.

Заканчылася тэрмін дзейнасці сесіі таксама заслуговае на свой окремы тэкст:

Your session has expired.
Please sign in again.

Тое ж самае стосуецца і проблем з правамі:

You don't have permission to access this resource.

Хораша паведамленне адпавяда на тры запытанні:

  1. Што пайшло не так?
  2. Чы робіць калявек трэба?
  3. Што ён можа зрабіць далей?

Супаравянанне кодаў статусу з паведамленнямі ў аднам месцы дапамагае падтрымваць адносовую цэласнасць тэксту.

Прабуйце зноў пасля тымчасовых абяканняў, але толькі для безпечных

Дзеянняя ўстаюць на час, напрыклад, перыядычныя проблемы з сетю. Для такіх случаў RxJS аддае метод retry().

this.http.get('/api/accounts')
  .pipe(
    retry(2)
  )
  .subscribe({
    next: (data) => {
      this.accounts = data;
    },
    error: () => {
      this.errorMessage =
        'Unable to load accounts.';
    }
  });

retry(2) прабуея зноў аж два разы, прычаму ўсё гэта робіцца перш чым паказаеся калебачанне error. Адтак, як аб’екты observables у HttpClient є «холаднымі», кожная новая прабаўка адправляе новы запыт.

У гэтым і крыўдза. Якшо запыт аб платежы успеў на серверы, але яго адпаведзь згубілася, прабаўка можа ўзимкаваць грошы у кляўэнта два разы, якшо толькі бэкенд не падтрымвае ідемпотентнасць. Будзіце аберажны ўжо да автаматычных прабаўкаў для:

  • Платежоў
  • Заказаў
  • Стварэнне абанку
  • Адкорекцыя дадзейнаў
  • Аперацыі выдалення
  • Запыты типу GET, якія дазволяюць толькі чытанне, значна простэйшыя для разумення. Таксама згадайце, што функцыя retry() прабуе зноў, калі выйшае які-небудзь бяда, уключаючы 400 або 401, якія ніколі не будуць успешнымі; зазвычай трэба прабаваць зноў толькі у разе сетавых бядаў і адпаведзей типу 5xx.

    Паўтарэння запытаў з адлікам

    Негайна паўтарэнне часта выклікае проблемы на там жа перавантажаным сервере. Функцыя retry() дазволяе дадаць паузу між паўтарэннямі.

    import { retry } from 'rxjs';
    this.http.get('/api/accounts')
      .pipe(
        retry({
          count: 2,
          delay: 1000
        })
      )
      .subscribe();
    

    Запыт отрымае ўсьмо тры шансы, з адлікам у адну секунду. Параметр delay таксама можа прыймаць функцыю, якая дазволяе рэалізаваць экспансійную стратэгію адліку (кожная пауза будзе дыяўальней пакой), а таксама прыпініць паўтарэння для бядаў, якія не могуць быць успешнымі. Такая стратэгія запобегае таму, каб багато неудачных кліентаў адразу ж накіравалі напор на сервер, які ўсё яшчэ восстанавляецца.

    Спрытнае стаўленне да істэклых сесыяў як глобальная проблема

    Класычны сцэнарый: корыстнік ачынае прыёмнік ранацай і вяртаецца за калькі гадзін. Токен істэк, таму наступны запит не выйшаў.

    GET /api/accounts
    401 Unauthorized
    

    Інтэрцептор — гэта месца для выявлення такой ситуацыі:

    if (error.status === 401) {
      // Clear authentication state
      // Redirect to login
    }
    

    Распрацоўка гэтай проблемы ў аднам месцы значыць, што жадны компанент не павінен пераўтвараць механізм істэкання сесыяў. За дапамогою токенав апавежэння інтэрцептор можа спробаваць ўпоркнуць іх перш чым перенаправіць; калі калькі запытоў не выйшлі адразу, пагляньце чаму адночасныя 401-ы выключаюць корыстнікаў для апрантку з адной спробай.

    Даўце корыстніку кантакт на прабую зноў

    Часта найэфектывнейшым засобам вярнуць стан у нормальны ўзгляд яє просты кантакт, які павторюе запыт.

    <div *ngIf="errorMessage">
      <p>{{ errorMessage }}</p>
      <button (click)="loadAccounts()">
        Try Again
      </button>
    </div>
    

    Уявіце корыстунача, у якога з’єднання перерываецца на калькі секундаў. Калі ён знову прайшоў у сеть, яму не трэба перзаванчваць прыкладненне і втрачаць стан; яму проста трэба нажыць:

    Try Again
    

    Пакалі loadAccounts() спачатку скасоўвае свой стан, ён служыць як для пачатковага заванчвання, так і для кожнай перапробавы. У шаблонах Angular 17+ можна вжываць @if заместо *ngIf.

    Вернуцца да застарэлых дадзеных, калі ўмоžнена их свежасць

    Якщо перзаванчванне панелі керування не выйшла, останнія заванчаныя цифры плюс чыстая працэска часта кращыя, чым порожняя стораніца.

    Showing your last available data.
    We couldn't refresh the dashboard.
    

    Кэш дазваляе гэта, а працэска є неабходная. Хадзіцеся ад патэрану, калі свежасць дадзеных є крытычная: балансы, цены акцый, запасы і дакладні вяроўкі платежаў должны паказваць адказку, а не застарэлыя значэнні.

    Запісваце достатню кантэкстную інфармацыю для дыбаггавання адказкі

    Скрыць деталі ад корыстнікаў не значыць ўтрымаць іх. Корыстны запис журналу мае нальот:

    Endpoint
    HTTP status
    Timestamp
    Request identifier
    User/session context where appropriate
    Error category
    

    Мінімальная версія інтэрцептора:

    console.error({
      endpoint: req.url,
      status: error.status,
      message: error.message
    });
    

    У прымэтнай сітцы ніхто не стежыць за консолью, таму трэба адправіць пакет дадзеных да службы монітарынгу адказоў, дзе ідэнтыфікатор запиту дазволяе супараванаваць яго з журналамі сервера. Ніколы не ліставаць токэнаў чы розных непатрэбных персональных дадзеных.

    Баланс, якога трэба прагты: корыстнік атрымле корыстную паведамленне, а разработчык — достатню колькасць деталей, каб знайсці прычыну.

    Не скрывайце адказоў пра абыекты за порожнімі рэзультатамі

    Пастойны спосаб — глытнуць адказ пра абыекты і вернуць порожняе значэнне:

    catchError(() => {
      return of([]);
    })
    

    Гэта тыха зміняе значэнне: пусты ўжоў стварае вражанне, што сервер вернуў нуль записаў. Корыстнік, які не бачыць жадных транзакцый, можа вывести, што ў яго ўсёўсе іх няма, а гэта горша, чым адказка пра адказку. Альтернатывныя рашэнні можуць быць правильнымі, але толькі свядома; інакш вы кантролюеце адказку пра адказку:

    catchError((error) => {
      return throwError(() => error);
    })
    

    Разделенне адпаведальнасцяў межы глобальным і спецыфічным для функцый керуванням

    Глобальнае керуванне падходзіць для прыбліжэнняў, якія маюць адно значэнне ў всіх месцах:

    401 → Authentication handling
    403 → Authorization handling
    500 → Generic server error
    

    Гэтыя прыбліжэнняя належаць у інтэрсептар. Тепер рассмотрзіце:

    POST /api/transfer-money
    

    Незуспэшны пераказ патрабуе конкрэтнага адказвання і чыстай заявы пра тое, чы роўнасць перасунулася, што генерычны адказ не можа даць. Функцыя павінна прымець адказку, таму інтэрсептар яе пракідае знову. Надзейны дизайн — глобальнае керуванне для спакойных прыбліжэнняя плюс керуванне, спецыфічнае для функцый, дзе важны бізнес-контэкст.

    Спісак перагляду для сцэнарыяў неудач

    Работа з адказамі на падбяры грунтуецца на проектаванні таго, што будзе вадзіцься пасля збою. Пераглядаючы якую-небудзь функцыя, запытайце:

    • Чы розумее корыстнік, што сталося?
    • Чы ён можа вярнуцца да нормальнага стану без перзапуску сторанкі?
    • Чы існуе можлівасць прабавы знову?
    • Чы індыкатор завантажэння завжды зупіняецца?
    • Чы тэхнічныя деталі не практыкуюцца ў інтэрфейсе?
    • Чы неудача фіксуецца там, дзе ўможліва яе пазірэнне для разработчыкаў?
    • Што будзе, калі токен выйдзе з часу падчас сесіі?
    • Што будзе, калі з’явіцца перыяд у сеті?
    • Што будзе, калі бэкенд вернуе даныя у неспадзеванай форме?

    Ключовыя выводы

    Неудачы ўсё часы з’являюцца, таму старайцеся пры ўсіх неудачах дзействаць грацыязна. У Angular гэта адбываецца за дапамогою:

    • Чыста выражаных станоў завантажэння, успеху і падбяру для кожнага запиту
  • finalize() – гэта дапамагае ўсунуць залишкі, незалежна ад таго, як завершыцца запит.
  • catchError() – выкорыстоўваецца для кантролю абяктаў, а не для ўховвання іх.
  • Інтэрцептор для заканчэння тымчасовага періоду сесіі, прызначэнняў і загальных абяктаў сервера; ён зноў выкалічвае абякты, каб функцыі моглі яшчэ рэагаваць.
  • Перапрыбуткі адбываюцца толькі для безпечных, ідемпотентных запитоў, з адлікам часу аб перерывамі.
  • Паведамленні, якія аб’являюць корыстніку, што сталося і што робіць далей.
  • Кнапкі для ручных перапрыбуткаў і, калі це дазволяе свежасць дадзеных, чытальна пазначаныя запасныя даны.
  • Структураваная логгінгавання, якая адправляецца да службы монітарынгу ў режыме працы.
  • Надзейныя програмы апісваюцца тым, як яны паводзяцца, калі ўтвараюцца проблемы.