Sisk

リクエスト

このページは英語から自動翻訳されています。 原文を読む

Requests は HTTP リクエストメッセージを表す構造体です。 HttpRequest オブジェクトは、アプリケーション全体で HTTP メッセージを処理するための便利な機能を提供します。

HTTP リクエストは、メソッド、パス、バージョン、ヘッダー、ボディで構成されます。

このドキュメントでは、これらの要素を取得する方法を解説します。

リクエストメソッドの取得 #

受信したリクエストのメソッドを取得するには、Method プロパティを使用します:

C#
static HttpResponse Index(HttpRequest request)
{
    HttpMethod requestMethod = request.Method;
    ...
}

このプロパティは、リクエストのメソッドを表す HttpMethod オブジェクトを返します。

注記

ルートメソッドとは異なり、このプロパティは RouteMethod.Any アイテムを提供しません。その代わりに、実際のリクエストメソッドを返します。

リクエスト URL コンポーネントの取得 #

リクエストの特定のプロパティを使用して、URL からさまざまなコンポーネントを取得できます。例として、次の URL を考えてみましょう。

http://localhost:5000/user/[email protected]
コンポーネント名説明コンポーネント値
Pathリクエストのパスを取得します。/user/login
FullPathリクエストのパスとクエリ文字列を取得します。/user/[email protected]
FullUrlリクエスト全体の URL 文字列を取得します。http://localhost:5000/user/[email protected]
Hostリクエストのホストを取得します。localhost
Authorityリクエストのホストとポートを取得します。localhost:5000
QueryStringリクエストのクエリ文字列を取得します。[email protected]
Query名前付き値コレクションとしてリクエストのクエリを取得します。{StringValueCollection object}
IsSecureリクエストが SSL を使用しているかどうかを判定します(true = 使用、false = 未使用)。false

また、HttpRequest.Uri プロパティを使用すれば、上記すべてを 1 つのオブジェクトで取得できます。

リクエストメタデータとキャンセル #

Sisk は各リクエストに運用メタデータも付与します。これらのプロパティは、ログ、トレース、ローカリゼーション、診断、長時間実行される操作に役立ちます。

プロパティまたはメソッド用途
RequestIdリクエストの一意な識別子です。IncludeRequestIdHeader を有効にすると X-Request-Id ヘッダーとして返されます。
RequestedAtSisk がリクエストオブジェクトを作成した時点です。
RemoteAddress接続から解決されたクライアントアドレス、または ForwardingResolver から取得されたアドレスです。
CultureAccept-Language から解決された最適なカルチャで、取得できない場合は現在のカルチャが使用されます。
DisconnectTokenクライアントが切断されたときに発行されるキャンセルトークンです(設定された HTTP エンジンがサポートしている場合)。
Bagリクエストハンドラやルートアクション間で共有される型付きキー/バリュー ストアです。
GetRawHttpRequest診断用のリクエストのテキスト表現です。

リクエストボディの取得 #

リクエストには、フォームやファイル、API 取引などのボディが含まれることがあります。ボディは以下のプロパティから取得できます。

C#
// リクエストのエンコーディングを使用して、リクエストボディを文字列として取得します
string body = request.Body;

// またはバイト配列として取得します
byte[] bodyBytes = request.RawBody;

// あるいはストリームとして取得できます。
Stream requestStream = request.GetRequestStream();

// 非同期でボディを読み取ることもできます
Memory<byte> bodyMemory = await request.GetBodyContentsAsync();

リクエストにボディが存在し、ロードされているかどうかは、HasContents プロパティ(リクエストにコンテンツがあるかを判定)および IsContentAvailable プロパティ(リモート側からコンテンツが完全に受信されたかを示す)で確認できます。

GetRequestStream でリクエストコンテンツを複数回読み取ることはできません。このメソッドで読み取った場合、RawBody と Body の値も利用できなくなります。リクエストストリームは、作成された HTTP セッションの終了時に自動的に破棄されるため、明示的に破棄する必要はありません。また、HttpRequest.RequestEncoding プロパティを使用すれば、リクエストを手動でデコードする際の最適なエンコーディングを取得できます。

サーバーはリクエストコンテンツの読み取りに制限を設けており、これは HttpRequest.Body と HttpRequest.RawBody の両方に適用されます。これらのプロパティは、入力ストリーム全体を HttpRequest.ContentLength と同サイズのローカルバッファにコピーします。

リクエストコンテンツは HttpServerConfiguration.MaximumContentLength によって制限されます。この設定は Sisk 1.7 以降デフォルトで 100 MB です(以前のバージョンでは 0(無制限)でした)。制限は以下のように適用されます:

  • Content-Length が上限を超えているリクエストは、ルートが呼び出される前に 413 Content Too Large 応答が返されます。
  • 長さが宣言されていないチャンク化またはストリーミングされたボディは、読み取り中にチェックされます。上限を超えて RawBody、Body、または GetRequestStream が返すストリームを読み込むと、InsufficientMemoryException がスローされ、コードまたは Router.CallbackErrorHandler が処理しない限り 500 Internal Server Error が返されます。
  • 同じ上限が受信した WebSocket メッセージの最大サイズにも適用されます。

プロパティを 0 に設定すると、制限が解除されます:

C#
using var app = HttpServer.CreateBuilder()
    .UseConfiguration(config => config.MaximumContentLength = 0)
    .Build();

設定された制限がなくても、Body と RawBody は Array.MaxLength(約 2 GB)を超えるコンテンツをバッファできず、アクセスしようとすると例外がスローされます。ストリーミングを使用すれば、より大きなコンテンツを扱うことが可能です。

注記

Sisk が許可している場合でも、HTTP のセマンティクスに従ってアプリケーションを構築し、許可されていないメソッドでコンテンツを取得または提供しないことが常に推奨されます。RFC 9110 “HTTP Semantics” を参照してください。

JSON リクエストの読み取り #

JSON API では、Body を読み取って手動でデシリアライズする代わりに、組み込みの JSON ヘルパーを使用してください。これらは System.Text.Json を使用し、デフォルトで HttpRequest.DefaultJsonSerializerOptions が適用されます。

C#
public record CreateUserRequest(string Name, string Email);

router.MapPost("/users", (HttpRequest request) =>
{
    CreateUserRequest? body = request.GetJsonContent<CreateUserRequest>();
    if (body is null)
        return new HttpResponse(System.Net.HttpStatusCode.BadRequest);

    return new HttpResponse(System.Net.HttpStatusCode.Created);
});

非同期ルート内で使用するか、リクエストのキャンセルでデシリアライズを中止したい場合は、非同期オーバーロードを使用してください。

C#
router.MapPost("/users", async (HttpRequest request) =>
{
    CreateUserRequest? body =
        await request.GetJsonContentAsync<CreateUserRequest>(request.DisconnectToken);

    if (body is null)
        return new HttpResponse(System.Net.HttpStatusCode.BadRequest);

    return new HttpResponse(System.Net.HttpStatusCode.Created);
});

特定のエンドポイント向けにカスタムの JsonSerializerOptions を渡すこともできます。

C#
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
    PropertyNameCaseInsensitive = true
};

UserDto? user = request.GetJsonContent<UserDto>(options);

Native AOT やトリミングに敏感なアプリケーションでは、JsonSerializerContext が生成する JsonTypeInfo<T> オーバーロードを使用してください。

C#
[JsonSerializable(typeof(CreateUserRequest))]
public partial class AppJsonSerializerContext : JsonSerializerContext
{
}

CreateUserRequest? body =
    await request.GetJsonContentAsync(
        AppJsonSerializerContext.Default.CreateUserRequest,
        request.DisconnectToken);

JSON ヘルパーにも同じ一度だけ読み取るルールが適用されます。Sisk が GetJsonContent、GetJsonContentAsync、Body、または RawBody を通じてリクエストストリームを読み取った後は、GetRequestStream() で同じボディを再度取得することはできません。

リクエストコンテキストの取得 #

HTTP Context は、HTTP サーバー、ルート、ルーター、リクエストハンドラ情報を格納する Sisk 固有のオブジェクトです。これらのオブジェクトが整理しにくい環境で、情報を一元管理するために使用できます。

現在実行中の HttpContext は、静的メソッド HttpContext.GetCurrentContext() で取得できます。このメソッドは、現在のスレッドで処理中のリクエストのコンテキストを返します。

C#
HttpContext context = HttpContext.GetCurrentContext();

ログモード #

HttpContext.LogMode プロパティを使用すると、現在のリクエストのロギング動作を制御できます。特定のリクエストに対してロギングを有効または無効にし、サーバーのデフォルト設定を上書きできます。

C#
// Disable logging for this request
context.LogMode = LogOutputMode.None;

リクエストバッグ #

RequestBag オブジェクトは、リクエストハンドラから別のポイントへ渡され、最終的な宛先で利用できる情報を格納します。このオブジェクトは、ルートコールバックの後に実行されるリクエストハンドラでも使用できます。

ヒント

このプロパティは HttpRequest.Bag プロパティからもアクセス可能です。

Middleware/AuthenticateUserRequestHandler.csC#
public class AuthenticateUserRequestHandler : IRequestHandler
{
    public string Identifier { get; init; } = Guid.NewGuid().ToString();
    public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse;
    
    public HttpResponse? Execute(HttpRequest request, HttpContext context)
    {
        if (request.Headers.Authorization != null)
        {
            context.RequestBag.Add("AuthenticatedUser", new User("Bob"));
            return null;
        }
        else
        {
            return new HttpResponse(System.Net.HttpStatusCode.Unauthorized);
        }
    }
}

上記のリクエストハンドラは AuthenticatedUser をリクエストバッグに設定し、最終コールバックで後から取得できます。

Controller/MyController.csC#
public class MyController
{
    [RouteGet("/")]
    [RequestHandler<AuthenticateUserRequestHandler>]
    static HttpResponse Index(HttpRequest request)
    {
        User authUser = request.Context.RequestBag["AuthenticatedUser"];
        
        return new HttpResponse() {
            Content = new StringContent($"Hello, {authUser.Name}!")
        };
    }
}

Bag.Set() と Bag.Get() ヘルパーメソッドを使用して、型シングルトンでオブジェクトを取得または設定できます。

TypedValueDictionary クラスは、より細かい制御のために GetValue と SetValue メソッドも提供します。

Middleware/Authenticate.csC#
public class Authenticate : RequestHandler
{
    public override HttpResponse? Execute(HttpRequest request, HttpContext context)
    {
        request.Bag.Set<User>(authUser);
    }
}
Controller/MyController.csC#
[RouteGet("/")]
[RequestHandler<Authenticate>]
public static HttpResponse GetUser(HttpRequest request)
{
    var user = request.Bag.Get<User>();
    ...
}

原子性コンテキスト値 #

リクエストバッグは同期されていません。同じリクエスト内で、ルートが開始した並列タスクなど、同時に数値が変更される可能性がある場合は、HttpContext.Interlocked を使用してください。これにより、リクエストバッグ内の名前付き値に対して原子的な操作を行う HttpContextInterlocked オブジェクトが取得できます。

メソッド説明
Add(name, value)value を既存の値に加算します。name が未設定の場合は value を保存します。
Exchange(name, value)保存されている値を置き換え、以前の値を返します。name が未設定の場合は value を返します。
CompareExchange(name, value, comparand, notFound = -1)保存された値が comparand と等しい場合にのみ value に置き換え、元の値を返します。name が未設定の場合は notFound を返します。
Inspect(name)保存された値を返します。name が未設定の場合は null を返します。

リクエストバッグでは値は double として保存されます。int や long を受け取るオーバーロードは、内部で double に変換します。

C#
router.MapPost("/import", async (HttpRequest request) =>
{
    string[] lines = request.Body.Split('\n');

    await Parallel.ForEachAsync(lines, async (line, cancellation) =>
    {
        if (await TryImportAsync(line, cancellation))
            request.Context.Interlocked.Add("imported", 1);
    });

    double imported = request.Context.Interlocked.Inspect("imported") ?? 0;
    return new HttpResponse($"{imported} line(s) imported");
});

注記

これらの操作は他の Interlocked 呼び出しに対してのみ原子的です。RequestBag を直接介して同じキーを読み書きする場合、これらの操作とは同期されません。

フォームデータの取得 #

StringKeyStoreCollection を使用して、以下の例のようにフォームデータの値を取得できます。

Controller/Auth.csC#
[RoutePost("/auth")]
public HttpResponse Index(HttpRequest request)
{
    var form = request.GetFormContent();

    string? username = form["username"];
    string? password = form["password"];

    if (AttempLogin(username, password))
    {
        ...
    }
}

リクエストボディが大きい場合やキャンセルサポートが必要な場合は、非同期バージョンが便利です。

C#
var form = await request.GetFormContentAsync(request.DisconnectToken);

マルチパートフォームデータの取得 #

Sisk の HTTP リクエストでは、ファイルやフォームフィールド、任意のバイナリコンテンツなど、アップロードされたマルチパートコンテンツを取得できます。

C#
// 以下のメソッドは、リクエストの入力全体を
// MultipartObject の配列に読み込みます
var multipartFormDataObjects = request.GetMultipartFormContent();

foreach (MultipartObject uploadedObject in multipartFormDataObjects)
{
    // マルチパートフォームデータで提供されたファイル名
    // オブジェクトがファイルでない場合は null が返されます。
    Console.WriteLine("File name       : " + uploadedObject.Filename);

    // マルチパートフォームデータオブジェクトのフィールド名
    Console.WriteLine("Field name      : " + uploadedObject.Name);

    // マルチパートフォームデータのコンテンツ長
    Console.WriteLine("Content length  : " + uploadedObject.ContentLength);

    // 各ファイルヘッダーに基づいて画像形式を判定します
    // 既知のコンテンツタイプです。認識できない一般的なファイル形式の場合、
    // 以下のメソッドは MultipartObjectCommonFormat.Unknown を返します
    Console.WriteLine("Common format   : " + uploadedObject.GetCommonFileFormat());
}

ルートが非同期の場合は、GetMultipartFormContentAsync を使用してください。

C#
var multipartFormDataObjects =
    await request.GetMultipartFormContentAsync(request.DisconnectToken);

Sisk の Multipart form objects とそのメソッド、プロパティ、機能の詳細は、こちらをご覧ください。

クライアント切断の検出 #

Sisk v1.15 以降、フレームワークは HttpRequest.DisconnectToken を通じてキャンセルトークンを提供します。設定された HTTP エンジンが切断検出をサポートしている場合、クライアント接続がレスポンス完了前に閉じられるとこのトークンがキャンセルされます。これにより、クライアントが結果を待っていない長時間実行される操作を停止できます。

C#
router.MapGet("/connect", async (HttpRequest req) =>
{
    // gets the disconnection token from the request
    var dc = req.DisconnectToken;

    await LongOperationAsync(dc);

    return new HttpResponse();
});

このトークンはすべての HTTP エンジンで互換性があるわけではなく、各エンジンで実装が必要です。

デフォルトの Sisk エンジン(System.Net.HttpListener ベース)はクライアント切断検出をサポートしていません。デフォルトエンジンを使用している場合、DisconnectToken は CancellationToken.None となり、実質的にキャンセルできないトークンとして扱われます。

Cadente engine は DisconnectToken をサポートしています。ルートが切断検出付きのキャンセルに依存する場合は、Cadente もしくはこの機能を明示的に実装した他のエンジンを使用してください。サポートされたエンジンでも、キャンセルは協調的に行われます。トークンを非同期 API に渡し、長時間実行する処理内でトークンをチェックしてください。

サーバー送信イベント (Server-sent events) のサポート #

Sisk は Server-sent events をサポートしており、チャンクをストリームとして送信し、サーバーとクライアント間の接続を維持できます。

HttpRequest.GetEventSource メソッドを呼び出すと、HttpRequest がリスナ状態になります。この状態では、サーバーサイドイベントで送信されるパケットと重複するため、HTTP リクエストのコンテキストは HttpResponse を期待しません。

すべてのパケットを送信した後、コールバックは Close メソッドを返す必要があります。これにより、サーバーに最終レスポンスが送信され、ストリーミングが終了したことが示されます。

送信されるすべてのパケットの総長さを予測できないため、Content-Length ヘッダーで接続の終了を判定することはできません。

ほとんどのブラウザのデフォルトでは、サーバーサイドイベントは GET 以外の HTTP ヘッダーやメソッドの送信をサポートしていません。そのため、特定のヘッダーが必要な event-source リクエストを扱うハンドラを使用する際は注意が必要です。

また、ほとんどのブラウザは、すべてのパケット受信後にクライアント側で EventSource.close が呼び出されない場合、ストリームを再開します。これによりサーバー側で無限に処理が続く可能性があります。この問題を回避するため、イベントソースがすべてのパケット送信を完了したことを示す最終パケットを送るのが一般的です。

以下の例は、ブラウザが Server-sent events をサポートするサーバーと通信する方法を示しています。

sse-example.htmlHTML
<html>
    <body>
        <b>フルーツ:</b>
        <ul></ul>
    </body>
    <script>
        const evtSource = new EventSource('http://localhost:5555/event-source');
        const eventList = document.querySelector('ul');
        
        evtSource.onmessage = (e) => {
            const newElement = document.createElement("li");

            newElement.textContent = `message: ${e.data}`;
            eventList.appendChild(newElement);

            if (e.data == "Tomato") {
                evtSource.close();
            }
        }
    </script>
</html>

そして、クライアントへ順次メッセージを送信します。

Controller/MyController.csC#
public class MyController
{
    [RouteGet("/event-source")]
    public async Task<HttpResponse> ServerEventsResponse(HttpRequest request)
    {
        var serverEvents = await request.GetEventSourceAsync ();
        
        string[] fruits = new[] { "Apple", "Banana", "Watermelon", "Tomato" };
        
        foreach (string fruit in fruits)
        {
            await serverEvents.SendAsync(fruit);
            await Task.Delay(1500);
        }

        return await serverEvents.CloseAsync();
    }
}

このコードを実行すると、以下のような結果が得られるはずです。

プロキシされた IP とホストの解決 #

Sisk はプロキシと併用できるため、クライアントからプロキシへの取引において IP アドレスがプロキシエンドポイントに置き換えられることがあります。

Sisk では、forwarding resolvers を使用して独自のリゾルバを定義できます。

ヘッダーエンコーディング #

ヘッダーのエンコーディングは、一部の実装で問題になることがあります。Windows では UTF-8 ヘッダーがサポートされていないため、ASCII が使用されます。Sisk には組み込みのエンコーディングコンバータがあり、誤ったエンコーディングのヘッダーをデコードするのに役立ちます。

この操作はコストが高く、デフォルトでは無効化されていますが、HttpServerConfiguration.NormalizeHeadersEncodings を有効にすることで使用可能になります。

ドキュメントと API リファレンスを検索するには入力してください。