Angular interceptor

Opublikowane przez lewy w dniu

Interceptory jak sama nazwa wskazuje służą do przechwytywania wywołań http i ich modyfikacji, a Angular udostępnia nam wbudowane narzędzia do ich łatwej obsługi.

Dzięki interceptorom możemy przechwycić globalnie każde nasze wywołanie http, dowolnie zmodyfikować jego zawartość i przekazać dalej. Możemy również za pomocą interceptora przechwytywać odpowiedź od serwera i wykonać na niej jakieś operacje.

Interceptorów możemy napisać wiele w naszej aplikacji i tworzyć łańcuchy wywołań interceptorów, ostatnie wywołanie w łańcuchu jest wykonaniem zapytania do serwera.

Aby zaimplementować interceptor piszemy serwis, który implementuje interfejs HttpInterceptor czyli zawiera metodę intercept

@Injectable()
export class MyInterceptor implements HttpInterceptor {

    constructor() {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req);
    }
}

Jest to najprostszy interceptor, który jest i tyle. Interceptor ten nic nie robi, przechwytuje wywołanie które zostało zainicjowane a następnie przekazuje je dalej bez żadnej modyfikacji.
req jest naszym obiektem zapytania i jest typu HttpRequest
next jest handlerem http i posiada metodę handle który przyjmuje jeden argument – nowy obiekt typu HttpRequest i zwraca HttpEvent w postaci strumienia, aby mógł być on dalej obsługiwany przez aplikację.

Aby nasz interceptor był w pełni używalny musimy o nim poinformować naszą aplikację, że chcemy go używać, robimy to w następujący sposób.

@NgModule({
    ...
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true },
    ],
    ...
})

W module w którym chcemy używać interceptora, musimy go dodać do sekcji providers, informujemy że jest to interceptor, podajemy naszą implementację i opcjonalnie jeżeli chcielibyśmy stosować kilka interceptorów podajemy wartość parametru multi na true.

Jak konkretnie obsługiwać/modyfikować zapytania i odpowiedzi za pomocą interceptorów, pokaże nam ten prosty przykład.

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log(`make request: ${req.url}`);
    return next.handle(req).pipe(
        tap((resp) => {
            if (resp instanceof HttpResponse) {
                console.log(`got response: ${resp.url}, status: ${resp.status}, body: ${JSON.stringify(resp.body)}`);
            }
        })
    );
}

Jak wspomniałem wcześniej funkcja intercept przyjmuje jako pierwszy parametr nasz obiekt zapytania i teraz możemy z nim zrobić co chcemy, w typ przypadku po prostu na konsoli wyświetlam informację że dane żądanie http zostało wywołane.

Natomiast to w jaki sposób obsłużyć odpowiedź od serwera widzimy w instrukcji return. Funkcja intercept zwrócić musi Observable z HttpEvent, ale jako że jest to strumień to możemy na nim wykonać jeszcze jakieś dodatkowe operacje zanim przekażemy go dalej. W powyższym przykładzie za pomocą funkcji tap przechwytuję obiekt odpowiedzi a następnie upewniając się że jest to HttpResponse wykonuję porządane przeze mnie operację, w tym przypadku wyświetlam informację o odpowiedzi z serwera.

Bardziej zaawansowanym przykładem użycia interceptorów i tym czego ja ostatnio potrzebowałem jest modyfikacja danych wychodzących do serwera i odbieranych z serwera. w aplikacji wszystkie property moich klas (modeli również) opisuję jako camelCase, natomiast serwer przyjmował (i zwracał) wartości obiektów zapisane za pomocą snake_case. Aby zapewnić taką konwersję danych w całej aplikacji w jednym miejscu mogę właśnie wykorzystać interceptor.

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.body) {
        req = req.clone({
            body: this.convertRequestBodyToSnakeCase(req.body),
        });
    }
    return next.handle(req).pipe(
        map((resp) => {
            if (resp instanceof HttpResponse) {
                resp = resp.clone({
                    body: this.convertResponseBodyToCamelCase(resp.body),
                });
            }
            return resp;
        })
    );
}

Na początek sprawdzam czy zapytanie posiada body, bo tylko je chce modyfikować, jeżeli tak to tworzę kopię zapytania ze zmodyfikowanym body, które zostanie ostatecznie przekazane do serwera. Następnie nasłuchując na odpowiedź z serwera, gdy ją otrzymam to za pomocą funkcji map modyfikuję otrzymany strumień danych, tym razem poprzez utworzenie kopii odpowiedzi ze zmodyfikowanym body.

Dla jasności jeszcze kod powyższych dwóch konwerterów:

private convertRequestBodyToSnakeCase(body: any): any {
    if (typeof body !== 'object') {
        return body;
    }
    if (isArray(body)) {
        return body.map((o) => this.convertRequestBodyToSnakeCase(o));
    }
    return mapKeys(body, (v, k) => snakeCase(k));
}

private convertResponseBodyToCamelCase(body: any): any {
    if (typeof body !== 'object') {
        return body;
    }
    if (isArray(body)) {
        return body.map((o) => this.convertResponseBodyToCamelCase(o));
    }
    return mapKeys(body, (v, k) => camelCase(k));
}

Metody te wykorzystują funkcje biblioteki lodash – mapKeys, camelCase, snakeCase.

Prosty kod z przykładami z tego wpisu można u mnie na githubie.

Innymi częstymi zastosowaniami interceptorów jest np. autentykacja, caching czy modyfikacja nagłówków. Wszystko co dotyczy zapytań http i chcemy aby było obsłużone globalnie w całej aplikacji nadaje się do wykorzystania interceptórów.