WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

応答ヘッダが 64KB を超えるとエラー

by WebSurfer 2019年9月2日 12:00

HttpClient を使った HPPT 通信で、応答ヘッダが 64KB を超えると WebException 例外がスローされるという話を備忘録として書いておきます。

(元の話は Teratail のスレッド「GetAsync処理時のメッセージの長さが制限の解消方法について」のもので、実際に自分が経験した訳ではなく聞いた話です)

同様な問題は HttpWebResponse / HttpWebRequest を使った時から起こっていた問題だそうで、応答ヘッダが 64KB を超えると WebException がスローされ、以下のエラーメッセージが出るそうです。

"接続が切断されました: メッセージの長さが制限を超えています。"

英文では、

"The underlying connection was closed: The message length limit was exceeded."

応答ヘッダが 64KB を超えるというのはレアなのか、日本語のエラーメッセージでググっても参考になる記事はヒットしませんでした。

でも、英語圏まで検索範囲を広げる(英文でググる)と HttpWebResponse / HttpWebRequest でこの問題に遭遇した人はいるようで、WebException: "The message length limit was exceeded" 他の記事がヒットします。

その記事に書いてある解決策は、HttpWebRequest の MaximumResponseHeadersLength プロパティを -1 (無制限) に設定することだそうです。(未検証・未確認です)

HttpClient を使う場合は、.NET Framework 4.7.1 以降ですが、HttpClientHandler クラスMaxResponseHeadersLength プロパティを使って応答ヘッダのサイズの許容最大値を設定できるそうです。

// Create an HttpClientHandler object
HttpClientHandler handler = new HttpClientHandler();
handler.MaxResponseHeadersLength = 128;  // 128KB

// Create an HttpClient object
HttpClient client = new HttpClient(handler);

.NET 4.5 では MaxResponseHeadersLength プロパティは使えず .NET 4.7.1 で使えるようになったということは、HttpWebRequest / HttpWebResponse で起こっていた問題に対応できないことを指摘されて追加したのかもしれませんね。(想像です)

Tags: , , ,

.NET Framework

HttpClient でファイルアップロード

by WebSurfer 2019年8月11日 14:00

HttpClient クラス を使った Windows Forms アプリで multipart/form-data 形式にてファイルをアップロードする方法を書きます。

HttpClient でファイルをアップロード

multipart/form-data 形式で送信するには MultipartFormDataContent クラスを利用します。

MultipartFormDataContent クラスのインスタンスを生成し、それに HttpContent クラスの派生クラスを multipart の各パートとして Add します。

この記事では文字列とファイルを別々のパートとして送信するサンプルを書きます。

その場合、使用する HttpContent クラスの派生クラスは、文字列を送信する場合は StringContent クラスを、ファイルを送信する場合は StreamContent クラスを使うのがよさそうです。

下のサンプルコードを見てください、StringContent クラスとStreamContent クラスを初期化してヘッダ情報を設定し、それぞれを MultipartFormDataContent オブジェクトに Add し、さらにそれを HttpRequestMessage オブジェクトの Content プロパティに設定しています。

using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

namespace WindowsFormsApplication2
{
  public partial class Form3 : Form
  {
    // socket 浪費問題対応
    private static HttpClient httpClient;

    public Form3()
    {
      InitializeComponent();
      this.textBox1.Text = "";
      this.label1.Text = "";
    }

    // ファイル選択に OpenFileDialog 利用
    private void button1_Click(object sender, EventArgs e)
    {
      OpenFileDialog openFileDialog1 = new OpenFileDialog();

      openFileDialog1.InitialDirectory = 
                               @"C:\Users\surfe\Pictures";
      openFileDialog1.Filter = 
           "jpeg files (*.jpg)|*.jpg|All files (*.*)|*.*";
      openFileDialog1.FilterIndex = 1;
      openFileDialog1.RestoreDirectory = true;

      if (openFileDialog1.ShowDialog() == DialogResult.OK)
      {
        this.textBox1.Text = openFileDialog1.FileName;
      }
    }

    // HttpWebRequest を利用
    private void button2_Click(object sender, EventArgs e)
    {
      // ・・・コードは省略・・・
    }

    // HttpClient を利用
    private async void button3_Click(object s, EventArgs e)
    {
      if (string.IsNullOrEmpty(this.textBox1.Text)) return;

      this.label1.Text = "";

      if (httpClient == null)
      {
        httpClient = new HttpClient();
      }

      //送信するファイルへのパス
      string filePath = this.textBox1.Text;
      string fileName = Path.GetFileName(filePath);

      string url = @"送信先 Web サーバーの URL";

      MultipartFormDataContent content = 
                              new MultipartFormDataContent();
            
      // ファイルのみでなく文字列も送信してみる
      string strData = "これは、テストです。";
      StringContent stringContent = new StringContent(strData);
      stringContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
      {
        Name = "comment"
      };
      content.Add(stringContent);

      // アップロードするファイル
      using (FileStream fs = new FileStream(filePath, 
                                            FileMode.Open, 
                                            FileAccess.Read))
      {
        StreamContent streamContent = new StreamContent(fs);
        streamContent.Headers.ContentDisposition = 
               new ContentDispositionHeaderValue("form-data")
        {
          Name = "upfile",
          FileName = fileName
        };                
        streamContent.Headers.ContentType = 
                    new MediaTypeHeaderValue("image/jpeg");
        content.Add(streamContent);

        // メソッド (POST) と送信先の URL 指定
        HttpRequestMessage request = 
                 new HttpRequestMessage(HttpMethod.Post, url);
        request.Content = content;

        // ここでファイルを HTTP ストリーム��書き込むので、
        // 以下は using の { } 内にないとファイルが読めな
        // いというエラーになる
        HttpResponseMessage response = 
                           await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                   await response.Content.ReadAsStreamAsync())
        {
          using (StreamReader sr = 
              new StreamReader(responseStream, Encoding.UTF8))
          {
            this.label1.Text = sr.ReadToEnd();
          }
        }
      }            
    }
  }
}

上記のコードを実行して、HttpClient を使ってファイルを送信し、返ってきた応答を表示したのが上に表示した画像です。

下の画像は Fiddler でのキャプチャ結果で、送信ヘッダとコンテンツを表示しています。送信ヘッダに指定されている boundary で StringContent (文字列) とStreamContent (ファイル) の部分が分けられ、各パートに Content-Disposition などのヘッダ情報が付与されているのが分かるでしょうか?

送信ヘッダとコンテンツ

最後になりましたが HttpClient を使う際の注意点として重要なことを書いておきます。それは、using 句を使うなどしてHttpClient の初期化と Dispose を繰り返すと socket が浪費されるという問題があるということです。詳しくは以下の記事を見てください。static にして使い回すのが良いとのことです。

YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

Tags: ,

Upload Download

HttpClient で WCF サービスを呼出

by WebSurfer 2018年2月24日 23:49

先の記事「HttpWebRequest で WCF サービスを呼出」では、JSON 文字列をデータとしてやり取りする WCF サービスのメソッドを HttpWebRequest / HttpWebResponse を利用して呼び出して JSON 文字列のデータを取得し、それを逆シリアル化して C# のオブジェクトに変換する方法を書きました。

HttpWebRequest / HttpWebResponse に代えて HttpClient を利用し、同じ WCF サービスのメソッドを呼び出してデータを取得するサンプルを書きます。 (呼び出し先の WCF サービスは「WCF と jQuery AJAX」を参照)

参考にしたのは Call a Web API From a .NET Client (C#) という記事ですので合わせて見ていただくと良いと思います。

まず、先の HttpWebRequest / HttpWebResponse を利用した記事と同様に、以下のクラス / データコントラクト定義を行います。

[DataContract]
public class RootObject
{
    // GetCarsByDoorsResult は WCF サービスメソッドに付与した
    // BodyStyle = WebMessageBodyStyle.WrappedRequest による
    // ラップの名前(ラップするのはセキュリティ対策)
    [DataMember]
    public List<Car> GetCarsByDoorsResult { get; set; }
}

[DataContract]
public class Car
{
    [DataMember]
    public string Make { get; set; }

    [DataMember]
    public string Model { get; set; }

    [DataMember]
    public int Year { get; set; }

    [DataMember]
    public int Doors { get; set; }

    [DataMember]
    public string Colour { get; set; }

    [DataMember]
    public float Price { get; set; }
}

参考にした Microsoft の記事ではデシリアライズに HttpContentExtensions.ReadAsAsync<T> メソッド (HttpContent) を使っていますが、今回の例のような入れ子になったオブジェクトはデシリアライズできないようです。

なので、先の記事と同様、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズすることにしました。

HttpClient.SendAsync メソッドを利用して、JSON 文字列を WCF サービスの GetCarsByDoors(int doors) メソッドに POST 送信します。以下のコードの例では 5 ドア車を要求しています。

応答の HttpContent が HttpClient.SendAsync メソッドの戻り値として返されますので、DataContractJsonSerializer クラスを利用して応答ストリームに含まれる JSON 文字列を C# のオブジェクトにデシリアライズします。

詳しくは以下のサンプルコードとそれに付与したコメントを見てください。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Threading.Tasks;
using System.Net.Http;
using System.IO;

namespace ConsoleAppHttpClientWCF
{
  class Program
  {
    static void Main(string[] args)
    {
      // GetCarsAsync メソッドの引数で 5 ドアを要求
      RootObject rootObject =
          GetCarsAsync(5).GetAwaiter().GetResult();
      ShowCars(rootObject);
    }

    static async Task<RootObject> GetCarsAsync(int doors)
    {
      using (HttpClient httpClient = new HttpClient())
      {
        string url = "http://.../GetCarsByDoors";

        // POST 送信を指定
        HttpRequestMessage request =
          new HttpRequestMessage(HttpMethod.Post, url);

        // POST 送信する JSON 文字列
        string postData = 
            "{\"doors\":" + doors.ToString() + "}";

        // Content-Type: application/json; charset=utf-8 が
        // 要求ヘッダに必要。それを POST 送信する JSON 文字
        // 列と共にここで設定
        request.Content = new StringContent(postData, 
                                          Encoding.UTF8, 
                                          "application/json");

        var response = await httpClient.SendAsync(request);

        // 応答のコンテンツを Stream として取得
        using (Stream responseStream = 
                await response.Content.ReadAsStreamAsync())
        {
          // JSON シリアライザの初期化
          DataContractJsonSerializer ser =
            new DataContractJsonSerializer(typeof(RootObject));

          // 応答のコンテンツを逆シリアル化して C# の
          // オブジェクトを取得
          RootObject rootObject =
              (RootObject)ser.ReadObject(responseStream);

          return rootObject;
        }                
      }
    }

    // コンソールに取得結果を書き出すためのヘルパメソッド
    static void ShowCars(RootObject rootObject)
    {
      foreach (Car car in rootObject.GetCarsByDoorsResult)
      {
        Console.WriteLine("Make:{0}, Model:{1}, Doors:{2}",
                          car.Make, car.Model, car.Doors);
      }
    }
  }

  /*
  結果は:
  Make:Audi, Model:A4, Doors:5
  Make:Ford, Model:Focus, Doors:5
  Make:Renault, Model:Laguna, Doors:5
  Make:Toyota, Model:Previa, Doors:5
  */
}

DataContractJsonSerializer クラスを利用したシリアル化 / 逆シリアル化については、MSDN ライブラリの記事「方法 : JSON データをシリアル化および逆シリアル化する」が参考になると思います。

なお、本題とは直接関係ないことですが、You're using HttpClient wrong and it is destabilizing your software によると、HttpClient の初期化と Dispose を繰り返すようなことをすると、socket が浪費されるという問題があるそうです。これは覚えておいた方がよさそうです。

Tags: ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar