programming/netfx/tips/httpclient_get_contentencoding/index.wiki.txt

current previous
1,169 0,0
+
%smdncms%(set-metadata,title,HttpResponseMessageからContent-Encodingヘッダを読み取る)
+
%smdncms%(set-metadata,keywords,HttpClient,Content-Encoding)
+
%smdncms%(set-metadata,tags,api/.net,lang/c#)
+
%smdncms%(set-metadata,alternative-document-versions,codelang=cs)
+

         
+
&msdn(netfx,type,System.Net.Http.HttpClient){HttpClientクラス};で&msdn(netfx,member,System.Net.Http.SocketsHttpHandler.AutomaticDecompression){AutomaticDecompressionプロパティ};を使用して``Content-Encoding``ヘッダに従った自動的なコンテンツの展開を行うと、``Content-Encoding``ヘッダの値が空として取得される問題と、その対応策について。
+

         
+
$relevantdocs(関連するIssue)
+

         
+
- [[Accessing original Content-Encoding header in HttpResponseMessage · Issue #42789 · dotnet/runtime:https://github.com/dotnet/runtime/issues/42789]]
+
- [[`System.Net.Http.HttpClient` empty `Content.Headers.ContentLength` when using `AutomaticDecompression` · Issue #82737 · dotnet/runtime:https://github.com/dotnet/runtime/issues/82737]]
+

         
+
$relevantdocs$
+

         
+
以下の内容は2023年05月05日時点での状況に基づく。
+

         
+
*前提・AutomaticDecompressionを使用した自動的な展開 [#httpclient_automatic_decompression]
+
はじめに前提知識として、&msdn(netfx,type,System.Net.Http.HttpClient){HttpClientクラス};で圧縮アルゴリズムを有効にする方法について。
+

         
+
まず、&msdn(netfx,member,System.Net.Http.SocketsHttpHandler.AutomaticDecompression){AutomaticDecompressionプロパティ};に対して有効にする圧縮アルゴリズムを指定した&msdn(netfx,type,System.Net.Http.SocketsHttpHandler){SocketsHttpHandler};もしくは&msdn(netfx,type,System.Net.Http.HttpClientHandler){HttpClientHandler};を作成する。 これにより、リクエスト送信時に``Accept-Encoding``ヘッダが自動的に設定されるようになる。 これ以降ではSocketsHttpHandlerを使用する。
+

         
+
続いて、作成したSocketsHttpHandlerを使用するHttpClientを作成し、リクエストを送信する。 これにより、レスポンスとして返送される``Content-Encoding``ヘッダの内容に応じて、自動的にコンテンツが展開される。
+

         
+
$samplecode(lang=c#,copyright-year=2023,HttpClientクラスでAccept-Encoding・Content-Encodingヘッダを扱う)
+
#code{{
+
using System;
+
using System.Net;
+
using System.Net.Http;
+

         
+
using var httpClient = new HttpClient(
+
  // リクエストの送受信時に使用するSocketsHttpHandlerを作成・指定する
+
  // HttpClientHandlerの場合も同様
+
  handler: new SocketsHttpHandler() {
+
    // Content-Encodingヘッダの内容に応じて、コンテンツの自動的な展開を行う
+
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
+
  },
+
  // このHttpClientのDisposeと同時にSocketsHttpHandlerもDisposeする
+
  disposeHandler: true
+
);
+

         
+
// リクエストの送信・レスポンスの受信を行う
+
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://example.test/"));
+
using var response = httpClient.Send(request);
+

         
+
// 受信したコンテンツを文字列として読み取る
+
// ここで、Content-Encodingヘッダで圧縮アルゴリズムが指定されていれば、
+
// その指定に応じて自動的に展開が行われる
+
Console.WriteLine(await response.Content.ReadAsStringAsync());
+

         
+
// ただし、サーバーからContent-Encodingヘッダが返送されていたとしても、
+
// HttpResponseMessage.Content.Headers.ContentEncodingプロパティの値は常に空になる
+
Console.WriteLine("Content-Encoding = '{0}'", string.Join(", ", response.Content.Headers.ContentEncoding));
+

         
+
// Content-Encoding = '' と表示される
+
}}
+
$samplecode$
+

         
+
このとき上記のコードにあるように、&msdn(netfx,member,System.Net.Http.Headers.HttpContentHeaders.ContentEncoding){HttpResponseMessage.Content.Headers.ContentEncodingプロパティ};は、実際にサーバーから返送された``Content-Encoding``ヘッダの内容ではなく、常に空になる。
+

         
+
SocketsHttpHandlerはAutomaticDecompressionプロパティに従って自動的な展開を行うと同時に、それ以上展開が必要ない・ヘッダの内容が意味を成さないことを表すために、ContentEncodingを空に設定した&msdn(netfx,type,System.Net.Http.HttpContent){HttpContent};を返す動作になっている。
+

         
+
また同様に、&msdn(netfx,member,System.Net.Http.Headers.HttpContentHeaders.ContentLength){HttpResponseMessage.Content.Headers.ContentLengthプロパティ};もnullに設定される動作になっている。
+

         
+

         
+

         
+
*対応策
+

         
+
**AutomaticDecompressionを無効にして自前で処理する [#disable_automatic_decompression]
+
&msdn(netfx,member,System.Net.Http.Headers.HttpContentHeaders.ContentEncoding){ContentEncoding};および&msdn(netfx,member,System.Net.Http.Headers.HttpContentHeaders.ContentLength){ContentLength};が空(もしくは``null``)に設定される動作は、&msdn(netfx,member,System.Net.Http.SocketsHttpHandler.AutomaticDecompression){AutomaticDecompressionプロパティ};を使用して自動的な展開を行う場合に特有の動作となっている。
+

         
+
そこで、``Accept-Encoding``ヘッダの設定および``Content-Encoding``ヘッダの取得、コンテンツの展開を手動で行うことにより、この問題を回避できる。 ただし、SocketsHttpHandler・HttpClientHandlerが内部で行っていることを再実装する形になるので、若干コードが複雑になる。
+

         
+
$samplecode(lang=c#,copyright-year=2023,AutomaticDecompressionを使用せず、Accept-Encoding・Content-Encodingヘッダの処理を自前で行う)
+
#code{{
+
using System;
+
using System.IO;
+
using System.IO.Compression;
+
using System.Linq;
+
using System.Net.Http;
+
using System.Net.Http.Headers;
+

         
+
// HttpMessageHandlerは指定せず、デフォルトのHttpMessageHandlerを使用するHttpClientを作成する
+
using var httpClient = new HttpClient();
+

         
+
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://example.test/"));
+

         
+
// 手動でAccept-Encodingヘッダの値を設定する
+
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
+
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
+
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("br"));
+

         
+
// リクエストの送信・レスポンスの受信を行う
+
using var response = httpClient.Send(request);
+

         
+
// この場合、ContentEncodingプロパティはサーバーから返送されたContent-Encodingヘッダの内容をそのまま返す
+
Console.WriteLine("Content-Encoding = '{0}'", string.Join(", ", response.Content.Headers.ContentEncoding));
+

         
+
// 一方、コンテンツの展開は手動で行う必要がある
+
var contentStream = response.Content.ReadAsStream();
+

         
+
// Content-Encodingヘッダに指定されているのと逆順で展開を行うStreamを作成する
+
foreach (var method in response.Content.Headers.ContentEncoding.Reverse()) {
+
  contentStream = method switch {
+
    "gzip" => new GZipStream(contentStream, CompressionMode.Decompress),
+
    "deflate" => new DeflateStream(contentStream, CompressionMode.Decompress),
+
    "br" => new BrotliStream(contentStream, CompressionMode.Decompress),
+
    _ => throw new NotSupportedException(),
+
  };
+
}
+

         
+
Console.WriteLine(new StreamReader(contentStream).ReadToEnd());
+
}}
+
$samplecode$
+

         
+

         
+

         
+
**HttpResponseMessage.Contentの型情報から推測する (不完全) [#guess_from_type_of_httpcontent]
+
&msdn(netfx,member,System.Net.Http.SocketsHttpHandler.AutomaticDecompression){AutomaticDecompressionプロパティ};を使用して自動的な展開を行った場合、&msdn(netfx,member,System.Net.Http.HttpResponseMessage.Content){HttpResponseMessage.Contentプロパティ};には展開済みの&msdn(netfx,type,System.Net.Http.HttpContent){HttpContent};が設定される。
+

         
+
そこで、HttpResponseMessage.Contentプロパティに設定されているオブジェクトの型情報を取得することによって、適用されていた圧縮アルゴリズムを推測する。
+

         
+
この方法では、``Content-Encoding: deflate, gzip``のように複数の圧縮アルゴリズムが適用されていた場合は、最後に展開されたアルゴリズムのみしか推測することができない。 また、公開されていない型の型名に依存した判定方法であるため、将来の変更によって動作が変わる可能性があり、結果は保証されない。
+

         
+
$samplecode(lang=c#,copyright-year=2023,HttpResponseMessage.Contentの型情報からContent-Encodingヘッダの内容を推測する)
+
#code{{
+
using System;
+
using System.IO;
+
using System.Net;
+
using System.Net.Http;
+

         
+
using var httpClient = new HttpClient(
+
  // リクエストの送受信時に使用するSocketsHttpHandlerを作成・指定する
+
  handler: new SocketsHttpHandler() {
+
    // Content-Encodingヘッダの内容に応じて、コンテンツの自動的な展開を行う
+
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
+
  },
+
  // このHttpClientのDisposeと同時にSocketsHttpHandlerもDisposeする
+
  disposeHandler: true
+
);
+

         
+
// リクエストの送信・レスポンスの受信を行う
+
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("http://example.test/"));
+
using var response = httpClient.Send(request);
+

         
+
// ここで、HttpResponseMessage.Content.Headers.ContentEncodingプロパティの値は実際のContent-Encodingの値を表さない
+
Console.WriteLine("Content-Encoding = '{0}'", string.Join(", ", response.Content.Headers.ContentEncoding));
+

         
+
// 代わりとして、HttpResponseMessage.Contentの型情報をもとに推測を行う
+
// HttpResponseMessage.Contentに設定されるオブジェクトは、圧縮アルゴリズムごとに
+
// 定義された型となっているため、その型名から圧縮アルゴリズムの推測を行う
+
// ただし、"Content-Encoding: deflate, gzip"のように複数の圧縮アルゴリズムが
+
// 適用されていた場合は、その最初に適用されたアルゴリズムしか推測できない
+
Console.WriteLine(
+
  "Content-Encoding (from type of Content) = '{0}'",
+
  response.Content.GetType().FullName switch {
+
    "System.Net.Http.DecompressionHandler+GZipDecompressedContent" => "gzip",
+
    "System.Net.Http.DecompressionHandler+DeflateDecompressedContent" => "deflate",
+
    "System.Net.Http.DecompressionHandler+BrotliDecompressedContent" => "br",
+
    _ => "",
+
  }
+
);
+

         
+
Console.WriteLine(await response.Content.ReadAsStringAsync());
+
}}
+
$samplecode$
+

         
+

         
+
*その他の情報
+
カスタム処理を記述した&msdn(netfx,type,System.Net.Http.DelegatingHandler){DelegatingHandler};を既存処理に挟み込む方法については、公開APIを使ってレスポンスの受信から``Content-Type``ヘッダの上書きまでの間にカスタム処理を挟み込む方法がないため、実装できない。