Obsługa błędów w aplikacji webowej

0

W sumie temat trochę z pogranicza designu aplikacji, więc może nie najlepszy dział wybrałem, aczkolwiek korzystam z Javy i Springa, tak więc jakieś technologiczne ograniczenia na pewno też wchodzą w grę.

Jako pracę inżynierską projektuję, implementuję i wdrażam aplikację. Z grubsza, jest to trochę przerośnięty CRUD, w środku siedzi jednak trochę dodatkowej logiki. Obecnie pracuję nad backendem, docelowo jednak powstanie również frontend, korzystający z API wystawionego przez wspomniany backend.

No i zastanawiam się jak rozwiązać obsługę błędów. Obecnie próbuje podejście, w którym wydzieliłem 3 kategorie błędów

  • constraintViolations - wszelkie błędy typu "to pole nie może być nullem", generalnie mógłbym tutaj zakończyć pracę na dodaniu adnotacji @NotNull na wejściowych DTOsach i w controllerach dorzucić @Valid @RequestBody. Spring sam ogarnie co z tym zrobić, jednak jako że chciałbym mieć jeden spójny ErrorResponse dla wszystkich przypadków, to po prostu samemu będę wołać te walidację w jakimś interceptorze i mapować to na swoje błędy.
  • domainError - wszelkie błędy, które wyszły z serwisu (ale nie wyjątki). Korzystam z vavrowego Either i każdy serwis który robi jakąś logikę ma po lewej stronie listę enumów opisujacych kolejno co poszło nie tak.
  • exception - czyli wszystko co faktycznie sypnie wyjątkiem i zmapuje się na HTTP 500, czyli jakiś błąd z bazy danych czy niedostępność zewnętrznego API które wołam w aplikacji. Łapie taki wyjątek w @ControllerAdvice i wrzucam do exception opisanego poniżej.

No i to wszystko opakowałem w coś takiego

@Getter
public class ErrorResponse {
    private final List<ConstraintViolation> constraintViolations;
    private final List<DomainError> domainErrors;
    private final ExceptionWrapper exception;
 
    // static factories
}

No i zastanawiam się jak to najlepiej obsłużyć na UI

  • Http 5xx -> redirect na stronę z informacją "coś poszło nie tak, spróbuj ponownie później". Na dobrą sprawę, ten exception mi w response nie jest aż tak potrzebny z punktu widzenia klienta
  • Http 401/403 -> redirect do logowania
  • Http 400 -> czyli jakiś constraintViolation, wiem wtedy że muszę szukać w tym miejscu i jakoś to wyświetlić. Frontend jednak będzie mimo wszystko mieć walidację na formularzach, jak zostawię pustę pole email to od razu lepiej mieć o tym informację, a nie dostawać ją dopiero z backendu.
  • Http 409 -> tutaj chciałbym wrzucić wszystkie błędy z domeny, nie wiem czy jest na to lepszy status, w każdym razie, wszystko co się tu nie uda powinno jakoś zostać przedstawione użytkownikowi.

Pytanie jednak, czy jest sens bawić się w robienie jednolitego ErrorResponse? Może wystarczy, żebym zwrócił domainError który ma jakieś znaczenie na UI i nie może być tam wcześniej walidowany i do tego włączyć @Valid na requestach w controllerach i tyle, nie przejmować się tym podwójnie, skoro i tak muszę to obsłużyć na frontendzie zanim wyślę request?

0

Napiszę swoją opinię, jak ja to widzę:

  • Czy zostawić jednolity ErrorResponse - jeśli zostawisz, to pewnie na froncie potem trzeba będzie sprawdzać które pole (constraintViolations domainErrors, exception) ma coś w sobie czy nie. Dlatego IMHO postawiłbym na zwracanie od razu ConstraintViolation, DomainError albo Exception z jakimś sensowym messegem
  • Czy "dublować" walidację, skoro na frontendzie też zachodzi - jak najbardziej. API backendowe musi być walidowane niezależnie od frontendu, ponieważ zawsze ktoś może sobie z palca http request wysłać
  • Co do kodu 409 - dość mało popularny status, ja bym błędy walidacyjne domenowe dał na 400, czyli jeśli użytkownik poda id produktu 5, no to pierwszą walidację przejdzie (bo 5 jest not null), ale np nie ma takiego produktu o takim id - to nadal jest błąd użytkownika a nie systemu, więc 400 Bad Request jest tu raczej sensowny (w przypadku akurat braku jakiegoś resource można się pokusić o 404, ale to już wyższa szkoła RESTa)
0

@Pinek:

Co do użycia 400 i 409, chodziło mi o proste rozróżnienie sytuacji, tzn.
Przypadek 1, w requeście nie podałem wymaganego pola (w sumie mało prawdopodobna sytuacja, ze względu na sprawdzenie tego na froncie, aczkolwiek muszę to też obsłużyć).
Dodatkowo, dla ułatwienia po prostu dodaje @NotBlank czy coś podobnego w dtosie i @Valid @RequestBody, dostaje:

{
    "cause": "Validation failed for argument [0] in org.springframework.http.ResponseEntity<pl.example.project.dto.result.CreateUserResult> pl.example.project.web.controllers.AuthenticationController.createUser(pl.example.project.dto.command.CreateUserCommand): [Field error in object 'createUserCommand' on field 'email': rejected value [null]; codes [NotBlank.createUserCommand.email,NotBlank.email,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [createUserCommand.email,email]; arguments []; default message [email]]; default message [nie może być odstępem]] "
}

Mało czytelne, ale działa bez dodatkowej pracy po mojej stronie.

Przypadek 2, podałem maila który jest zajęty

{
    "domainErrors": [
        "EMAIL_ALREADY_IN_USE"
    ]
}

Rozbijając to na dwa kody Http chciałem uniknąć czegoś takiego po stronie Angulara

 this.userService.createUser().subscribe(res => {
      // success
    }, err =>  {
        if(err.status === 400 && err.domainError) {
          err.domainError.array.forEach(element => {
            this.specificErrors.push(i18n(element));
          });
        } else {
          this.generalError = 'Something bad happend';
        }
      }
    );

1 użytkowników online, w tym zalogowanych: 0, gości: 1