サーバー側レンダリングするクライアントサイド Blazor で、HTTP 404 Not Found を返したい

# "お探しの商品は見つかりませんでした" ページを表示する、クライアントサイド Blazor アプリ

[前回の投稿](https://qiita.com/jsakamoto/items/25dcd56be8a573bc6827)で、クライアントサイド Blazor であっても、初回 Web サーバーへのアクセス時に返される HTML を事前レンダリングできることがわかりました。

さてところで、Web アプリの仕様によっては、
「かつては存在してたけど、今は削除された商品の、商品紹介ページを指す URL」
にアクセスされたら「お探しの商品は見つかりませんでした」という表示をするケースも多いかと思います。

そのような「お探しの商品は見つかりませんでした」に対応した Web アプリを、[前回の投稿](https://qiita.com/jsakamoto/items/25dcd56be8a573bc6827)の要領で、サーバー側レンダリング付きの Blazor で実装することを考えてみましょう。

イメージとしては、下記みたいな感じで、URL に埋め込まれた商品コードから "商品リポジトリ" を検索し、見つからなかったら `NotFound` フラグを立てる、みたいな実装ですかね。

```csharp
@page "/products/{code}"
@inject IMyProductRepository ProductsRepository
...
@code {
  [Parameter]
  public string code { get; set; }

  private Product Product;

  private bool NotFound;
  
  protected override void OnInitialized() {
    this.Product = this.ProductsRepository.FindByCode(code);
    if (this.Product == null) {
      this.NotFound = true;
    }
  }
}
```

で、`NotFound` フラグが立ってたら、「お探しの商品は見つかりませんでした」と表示する、と。


```html
@if (this.Product != null) {
  ... ここは商品が見つかった場合の、商品説明コンテンツ ...
}
@if (this.NotFound) {
  <p>お探しの商品は見つかりませんでした。</p>
}
```

これでわざと、存在しない商品コードを指す `http://localhost:5000/products/foobar` みたいな URL でアクセスすれば...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/e572c954-8f44-9f1a-e6bc-b9ec584a9c8a.png)


うん、たしかに「お探しの商品は見つかりませんでした」コンテンツが、サーバー側レンダリングされた HTML で返ってきてますね!

返ってきてます、が...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/8eb49df2-33bd-b0ec-0a09-f774923ddf79.png)


うーん、HTTP ステータスが 200 (成功)、ですかぁ...。

# HTTP ステータス 404 Not Found を返したい

まぁ、このような「お探しの商品は見つかりませんでした」ページが HTTP ステータス 404 を返さなくても、SEO 的には問題ないみたいなツイートをどこかで見たような気もするのですが、記憶があやふやです。

しかしここは折角なので、「お探しの商品は見つかりませんでした」ページがサーバー側レンダリングされるときには、返す HTTP ステータスを 404 にしてみましょう!

もっとも、サーバー側レンダリングするときの Blazor って、要するにサーバー側 ASP.NET Core 実装で動いてるわけですから、ものすごく特別なハックが必要なわけではありません。

ASP.NET Core 標準の、`IHttpContextAccessor` というインターフェースのサービスを DI 経由で入手して使うだけです。

具体的な手順を追ってみましょう。

## 1. サーバー側実装の変更

まずはサーバー側レンダリングしている、ASP.NET Core サーバー実装側からいじりましょう。

`IHttpContextAccessor` というインターフェースのサービスは、残念ながら(?)、既定では DI 機構には未登録のようです。
ですので、自分で、`Startup.cs` 内で登録してあげる必要があります。

具体的には、下記のように、`AddHttpContextAccessor()` 拡張メソッドの呼び出しによって登録を行ないます。

```csharp
using Microsoft.Extensions.DependencyInjection; // <- この名前空間を開いておく
...
public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddHttpContextAccessor(); // <- これを追加!
      ...
```

## 2. クライアント側実装の変更

次はクライアント側実装をいじります。

まずはクライアント側 Blazor プロジェクトでも `IHttpContextAccessor` インターフェースを参照できるよう、必要な NuGet パッケージ = `Microsoft.AspNetCore.Http.Abstraction` の追加を行ないます。

dotnet CLI をお使いであれば `dotnet add package Microsoft.AspNetCore.Http.Abstractions` するとか、あるいは下記のように、クライアント側 Blazor のプロジェクトファイル (.csproj) に直接、パッケージ参照を追記してもよいでしょう。

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  ...
  <ItemGroup>
    <!-- ↓このパッケージ参照を追加 -->
    <PackageReference 
      Include="Microsoft.AspNetCore.Http.Abstractions" 
      Version="2.2.0" />
    ...
```

これでクライアント側 Blazor 実装でも `IHttpContextAccessor` インターフェースを使用できるようになりました。

ということで、先ほど冒頭の「お探しの商品が見つかりませんでした」表示を行なうコンポーネントの先頭にて、`IHttpContextAccessor` インターフェースのサービスを DI 機構にて注入してもらうことにしましょう。

```csharp
@page "/products/{code}"
@inject IMyProductRepository ProductsRepository 

@using Microsoft.AspNetCore.Http @* ←これと.. *@
@inject IHttpContextAccessor HttpContextAccessor  @* ←これを追加 *@
...
```

こうして `IHttpContextAccessor` インターフェースのサービスを入手したら、このサービスを介して HTTP 応答ステータスをコンポーネント内から指定できるようになります。

先の実装例で、該当商品がなかったときに `NotFound` フラグを立てるところで、ついでに HTTP 応答ステータスに 404 を指定するようにします。

```csharp
  ...
  protected override void OnInitialized() {
    ...
    if (this.Product == null) {
      this.NotFound = true;

      // 以下の 2 行を追加!
      var httpContext = this.HttpContextAccessor.HttpContext;
      httpContext.Response.StatusCode = 404;
    }
  }
}
```

以上で実装は終了です。

再びブラウザでページをリロードしてみると...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/f28c255b-01a4-659f-b6c2-4d0d5a5ab48f.png)

やったね!
ちゃんと HTTP 404 Not Found ステータスが返ってます!


# クライアント側が破綻してない?

...と喜んだのも束の間。

あれれ?

ブラウザの表示が "Loading..." のまま (汗

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/0e2401a5-2564-d042-2ac2-1495e3e8444b.png)

ブラウザの開発者コンソールを確認すると、がっつり例外で落ちてました...orz

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/3944578a-1654-6fa8-5cbc-8db37b39f7b6.png)

それもそのはず。

上図エラーメッセージにも「 There is no registered service of type 'Microsoft.AspNetCore.Http.IHttpContextAccessor'.」と書いてあることからもわかるように、同じ Blazor コンポーネントであっても、クライアント側で動くときには、ブラウザ内の WebAssembly 上の mono.wasm による CLR 上で動いているわけで、その環境では `IHttpContextAccessor` インターフェースのサービスは DI 機構に登録されていないので、それで「そんなサービス見つからないよ！」と例外で落ちていたのでした。

そもそもブラウザ内での動作においては「HTTP 応答ステータス」の概念がありえないわけです。
JavaScript も Blazor も、クライアント側のプログラムって、「ユーザーエージェントからの HTTP 要求に対する応答」という動きじゃないですからね。

# 注入してもらうんじゃなくて、自分から取りに行くスタイルで!

さてさて、この状況を回避するには、

「DI 機構にて、自動で `IHttpContextAccessor` サービスを注入してもらう」

のではなく、

**「自分から `IHttpContextAccessor` サービスの取得を試みる」**

ことで実現可能です。

具体的には、クライアント側 Blaozr コンポーネントの実装を、あともう少し書き換えることでできますので、先の実装から引き続きやってみましょう。


先ほど改造した「お探しの商品が見つかりませんでした」表示を行なうコンポーネントにて、先頭に`IHttpContextAccessor` インターフェースのサービス注入を記述しましたが、いったんそれは削除します。

代わりに、`IServicePorvider` インターフェースのサービスを DI 機構にて注入してもらいます。

```csharp
@page "/products/{code}"
@inject IMyProductRepository ProductsRepository 
@using Microsoft.AspNetCore.Http

@* ↓ IHttpContextAccessor の注入を削除して、代わりにこれを追加 *@
@inject IServiceProvider ServiceProvider 
...
```

そして、同コンポーネント中で、この `ServiceProvider` に対して、`IHttpContextAccessor` サービスの取得を試みます。

非 null、すなわち `IHttpContextAccessor` サービスのインスタンスが手に入った場合は、それすなわちサーバー側レンダリング時です。
ですので、そのときは HTTP 応答ステータスを指定すればよいわけです。

```csharp
  ...
  protected override void OnInitialized() {
    ...
    if (this.Product == null) {
      this.NotFound = true;

      // 先ほど追加した 2 行は削除した上で...

      // IHttpContextAccessor サービスの取得を試行
      var accessor = ServiceProvider.GetService<IHttpContextAccessor>();

      if (accessor != null)
      {
        // IHttpContextAccessor サービスが取得できた場合に限り、
        // HTTP 応答ステータスの指定を実施
        var httpContext = accessor.HttpContext;
        httpContext.Response.StatusCode = 404;
      }
      ...
```

これでもういちど試してみると...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/c7462a40-47f8-33cb-8df3-8a386f76194d.png)

おめでとうございます!

以上の実装で、「お探しの商品は見つかりませんでした」が表示されるときは、サーバー側レンダリング時に HTTP 応答ステータスに 404 Not Found が返りつつ、クライアント側 Blazor の動作も破綻せずに表示されています。

# 最後にちょっと補足を

なお、クライアント側 Blazor プロジェクトにて、`IHttpContextAccessor` インターフェースを参照できるよう、NuGet パッケージ = `Microsoft.AspNetCore.Http.Abstraction` を追加した関係で、ブラウザに読み込まれる .dll ファイルの数、すなわち総転送ファイルサイズが若干増えてしまっています。
クライアント側での動作においては実質使われないにもかかわらず、です。

クライアント側での動作時にこのような余分な .dll ファイルの読み込みを抑止し、クライアント側 Blazor としての必要総ファイルサイズをなるべく増やさないようにするには、もう一工夫が必要です。

そのもう一工夫についてはまたの機会に。

Happy Coding! :)