HttpInterceptorのユースケース

v4.3で追加されたHttpClientの中で、特に`HttpInterceptor`について、ユースケースを挙げていきます。
対象はHttpClientの初級〜中級レベルの想定です。`やるよねえ`から`やるかもしれないなあ`くらいを取り上げたつもりです。また、`できないよ`というのも拾ってみました。

# HttpInterceptorについて

HttpInterceptorは、appが発行する`全てのhttpリクエストやレスポンスに処理を追加できる`仕組みです。

angularJSでもあったようなので古参のかたはお馴染みのようです。自分のような2以降に入った人にとっての説明としては、概念では、javaで言うAOPや、railsのcallback、reduxのmiddlewareなどが近いです。仕組みはそれぞれ違いますが、フックでしょ、フック。

HttpInterceptor、およびHttpClientの基本については、公式ドキュメントを翻訳したのでそちらをご参照ください。
[公式ドキュメント(日本語版)「基礎ガイド-HttpClient編」](https://angular.jp/guide/http)

公式ドキュメントでは、HttpInterceptorのユースケースがいくつかサンプルコードで取り上げられています。

- **ヘッダーにAuthorizationと認証トークンをつける**
- **アップロード/ダウンロードの進行状況を計算する**
- **レスポンス内容をキャッシュする**
- **urlを変える**
- **応答時間を計測する**

以下、公式ドキュメントに乗っていないユースケースを数点サンプルを挙げていきます。

# リクエストをインターセプトする

## ドメインを変える

**場面例:dev環境はCORS許可でやっていて、environmentでドメインを切り替えたい**

公式ドキュメントのurlをリプレースするサンプルを少し発展させたものです。
environmentにdomainを設定して、HttpApiInterceptorでurlにdomainを追加します。

```ts:environment.ts
export const environment = {
  production: false,
  domain: 'http://localhost:3000', // dev環境のAPIサーバ
};
```

```ts
import { environment } from 'environments/environment';

@Injectable()
export class HttpApiInterceptor implements HttpInterceptor {
  // 起動環境毎のAPIサーバを保持する
  readonly domain: string = environment.domain;

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const req = request.clone({
      url: this.domain + request.url, // ここでServiceで設定されたAPIパスにドメインをくっつける
    });
    return next.handle(req);
  }
}
```

## 複数のheader、またはparamsをセットする

**場面例:ヘッダーを複数のInterceptorで段階的に追加していきたい**

複数のheader、またはparamsをセットするために、一番シンプルな方法はこれです。

```ts
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  const req = request.clone({
    setHeaders: {
      test1: 'value1',
      test2: 'value2',
      test3: 'value3'
    },
    setParams: {
      param1: 'value1',
      param2: 'value2',
      param3: 'value3',
    }
  });
  return next.handle(req);
}
```

以下のように書き方を変えることもできます。本例では全く同じ動きになりますが、Interceptorを複数分けている時に、違いを発揮します。

```ts
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  // 引数requestからcloneして追加する
  const headers = request.headers
    .set('test1', 'value1')
    .set('test2', 'value2')
    .set('test3', 'value3');

  // 引数requestからcloneして追加する
  const params = request.params
    .set('param1', 'value1')
    .set('param2', 'value2')
    .set('param3', 'value3');

  const req = request.clone({
    headers: headers,
    params: params,
  });
  return next.handle(req);
}
```

前提にあるのは、**HttpRequestオブジェクトはイミュータブル**で扱われることです。このやり方をするときは、`new HttpHeaders() | new HttpParams()`ではなく`request.headers | request.params`から生成するのが無難です。setterは内部的にはcloneしてset()毎に別インスタンスを生成しています。そして、updateフィールドにset毎の値を格納して管理しているため、Interceptorを分けて作っている時などに`new HttpHeaders() | new HttpParams()`したインスタンスを放り込むと、前段の内容が上書きされて消えてしまいます。

逆に言うと、以下のように複数のInterceptorを用いて`header | params`を段階的に追加していくことが可能です。

```ts:first.interceptor.ts
// 一つ目のInterceptor
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  const headers = request.headers
    .set('test1', 'value1');

  const params = request.params
    .set('param1', 'value1');

  const req = request.clone({
    headers: headers,
    params: params,
  });
  return next.handle(req);
}
```

```ts:second.interceptor.ts
// 2つ目のInterceptor
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  const headers = request.headers
    .set('test2', 'value2')
    .set('test3', 'value3');

  const params = request.params
    .set('param2', 'value2')
    .set('param3', 'value3');

  const req = request.clone({
    headers: headers,
    params: params,
  });
  return next.handle(req);
}
```

なお、複数のInterceptorを定義する方法は後述しています。



# レスポンスをインターセプトする

## 送信ログを取る

**場面例:障害調査でrequestを発信したか担保取りたい。**

interceptorがrequestを掴み、next.handleをreturnしますが、next.handle()は`HttpEvent`を断続的に流すストリームになります。

例）HttpSentEvent → HttpProgressEvent　→ HttpProgressEvent　→ HttpResponse

HttpEventは5種類定義されていて、`HttpSentEvent`が送信イベントになります。

*HttpEventの種類*
`HttpSentEvent | HttpHeaderResponse | HttpResponse<T> | HttpProgressEvent | HttpUserEvent<T>`

よって、handleをdoなりmapなりのRxオペレーターで処理してHttpSentEventをマッチさせれば、**発信直後のストリームのスナップショット**が取れます。ちなみにHttpResponseはinstanceofでマッチしますが、HttpSentEventはただのハッシュ`{type: 0}`が実体のため、instanceofではなくtypeを比較します。

```ts
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  return next.handle(req)
          .do((event: HttpEvent<any>) => {
            // HttpSentEventをマッチする
            if (event.type === HttpEventType.Sent) {
              console.log('sent');
            }
          });
  }
}
```

また、service側で以下のようにobserveオプションを`events`に変えれば、HttpSentEventをsubscribeすることもできます。

```ts
this.httpClient.get(`/api/test`, {
  observe: 'events',
})
.subscribe(event => console.log('event', event)); // SentEvent→Responseが順番に流れてくる
```

## エラーをハンドリングする

**場面例:特定のステータスコードは特定の処理を行いたい**

以下のようにRxのcatchオペレーターでエラーを捕まえることができます。`HttpErrorResponse`の担保を取っておくと良いと思います。

```ts
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  return next.handle(req)
          .catch((error: any) => {
            if (error instanceof HttpErrorResponse) {
              switch (error.status) {
                case 304:
                  console.log('304');
                  return Observable.throw('304');
                case 401:
                  console.log('401');
                  return Observable.throw('401');
              }
            }
            return Observable.throw(error);
          });
  }
```

# 複数のInterceptorを定義する

## 単純にInterceptorを並べる

**場面例:記述が膨らんできたので責務毎にInterceptorを分割したい**

公式ドキュメントでも文章の説明がありますが、以下のように複数並べて供給するだけです。

```ts
providers: [
  { provide: HTTP_INTERCEPTORS, useClass: FirstInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: SecondInterceptor, multi: true }
]
```

処理の順番は、FirstInterceptor.interceptor() → SecondInterceptor.interceptor() で実行されます。ただし、**HttpEventの流れる順番は逆転します**。これは、Interceptorが数珠繋ぎになっていると、returnのnextは次のHttpInterceptorへの参照を持ち、順番にスタックに積まれてLIFO(LastIn-FirstOut)になっているためと思われます。最後のInterceptorのnextの実体が`HttpXhrBackend`になり、ようやくネイティブのxhrオブジェクトが生成されます。

## Module毎にInterceptorを分ける

**場面例:一つのAPP内で一般公開サイトと管理サイトを作っていて、共通処理を分けたい**

module毎にInterceptorを供給すると、DependencyInjectionTreeの解決順番によりInterceptorが並びます。

しかし、LazyLoadingModuleならDependencyInjectionTreeが独立するため、A moduleだけで動くA Interceptorと、B Moduleだけで動くB Interceptorといった定義も可能ではあります。

ただし、基本的な設計思想として**Interceptorは直列で繋いでいくもの**になっています。そのため、用途を分けたいなら、interceptorメソッド内で分岐を行うことが基本となり、並列で定義するのは道を外れた行為であることを自覚しDependencyInjectionTreeやwebpackのlazyModuleを十分に理解して行うべきと思います。

以下は一つのInterceptorで統合する場合のサンプルです。管理サイトはサブドメインで切られている想定で、サイトを判定して、サイト毎にreturnを分けています。

```ts
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  
  const site = location.hostname;
  // 管理サイトである
  if (site.match(/^admin/)) {
    // 管理サイトだけ認証する
    const req = request.clone({
      setHeaders: { Authorization: 'token' },
    });
    return next.handle(req);
  // 一般公開サイトである
  } else {
    // 一般公開サイトだけ別サービスに実績送信する
    return next.handle(request)
      .do(event => this.sendAnalystics(request.url));
  }
}
```

# できないこと

## HEADのレスポンスを判定する

HttpEventに`HttpHeaderResponse`があるので、サーバがHEADで返せば取れると思いきや無理でした。response.d.tsの`HttpHeaderResponse`には`A partial HTTP response which only includes the status and header data,but no response body.（ステータスとヘッダーだけを持ちボディを持っていない部分的なHTTPレスポンス）`とコメント書かれているので、取れるはずのようです。

ダメだったコードは以下に格納しています。（サーバレスポンスはexpressのend()で実行。他にもrailsのHEADメソッドで返してもHttpResponseで処理された。）
https://github.com/studioTeaTwo/angular-starter/commit/15d6ceba91df1d079a139a78a5f99fe09d92f05a


なお、HEADのレスポンス自体はHttpClientで以下のようにすれば、subscribeできます。

```ts
 this.httpClient.post(`/api/example/header`, {
   observe: 'response',  // ヘッダーを含めるために指定
   responseType: 'text', // bodyのjson.parse()を止めるためにダミーで指定する
 })
```

- `responseType=json`になっていると受信後パースが走るため実行時エラーになります。
- `post<T>`と型指定するとbodyの型チェックするためコンパイルエラーになります。


# 最後に

今回検証していたsandboxも上げておきます。
angularは`5.0.3`でやっていました。
https://github.com/studioTeaTwo/angular-starter/tree/example/http

明日4日目は@MasanobuAkibaさんです。