このマニュアルは現在作成中であり、未完成な部分があります。
改善にご協力いただければ幸いです。READMEをご覧ください。

7 基本HTTP

この章では、リクエストの解析、レスポンスのレンダリング、コンテンツネゴシエーション、ファイルアップロードなど、基本的なHTTPに関する処理方法について説明します。

1.7 リクエストとレスポンス

ハンドラーが操作するコンテキストオブジェクトは、getRequest()getResponse()メソッドを提供し、それぞれRequestResponseにアクセスできます。これらのオブジェクトは、ほぼ期待通りの機能を提供します。

例えば、両方ともgetHeaders()メソッドを提供し、リクエストと共に送信されたHTTPヘッダーのモデルと、レスポンスと共に送信されるHTTPヘッダーのモデルを返します。Requestは、HTTPメソッドURIクエリ文字列パラメーターのキーバリューモデルなどのその他のメタデータ属性を公開します。

2.7 リダイレクト

redirect(int, Object)コンテキストメソッドは、リダイレクトの実行をサポートします。このメソッドは、コンテキストレジストリからRedirectorを取得し、引数を転送します。

Ratpackは、以下をサポートするデフォルトの実装を提供します。

  1. リテラルURL値
  2. プロトコル相対URL値
  3. 現在のアプリケーション内の絶対パス
  4. 現在のアプリケーション内の相対パス

ほとんどのアプリケーションでは、デフォルトの動作で十分なため、カスタムRedirector実装を提供する必要はありません。カスタムリダイレクター実装を提供する理由の1つは、ドメインオブジェクトをリダイレクト先の場所として解釈することです。

3.7 リクエストの読み取り

リクエストの本文を取得するためのいくつかのメカニズムがあります。単純なユースケースでは、Context.parse(Class<T>)は、クラス全体をメモリにバッファリングし、指定された型のオブジェクトを生成します。リクエスト全体のテキストまたはバイトビューが必要な場合は、より低レベルのRequest.getBody()メソッドを使用できます。高度な用途や非常に大きなリクエストを処理する場合には、[Request.getBodyStream()]は受信した個々のバイトチャンクへのアクセスを提供します。

1.3.7 パーサー

リクエスト本文をオブジェクト表現に変換するパーサーメカニズム。コンテキストレジストリからParser実装を選択することで機能します。Context.parse(Class<T>)の詳細と追加のバリアントを参照してください。

1.1.3.7 JSON

Jacksonに基づいて、JSONリクエスト本文を扱うためのサポートがすぐに利用できます。Jackson parsingに例があります。

2.1.3.7 フォーム

RatpackはコアにFormオブジェクトのパーサーを提供します。これは、URLエンコードとマルチパート(ファイルアップロードを含む)の両方のPOSTされた(またはPUTされたなど)フォームの読み取りに使用できます。

import ratpack.core.handling.Handler;
import ratpack.core.handling.Context;
import ratpack.core.form.Form;
import ratpack.core.form.UploadedFile;

public class MyHandler implements Handler {
  public void handle(Context context) {
    Promise<Form> form = context.parse(Form.class);

    form.then(f -> {
      // Get the first attribute sent with name “foo”
      String foo = form.get("foo");

      // Get all attributes sent with name “bar”
      List<String> bar = form.getAll("bar");

      // Get the file uploaded with name “myFile”
      UploadedFile myFile = form.file("myFile");

      // Send back a response …
    });
  }
}

FormUploadedFileで、詳細と例を参照してください。

2.3.7 バイトとテキスト

Request.getBody()はリクエスト全体をメモリに読み込み、バイトまたは文字列としてデータにアクセスできるようにします。

このメソッドは、サーバーで構成された最大コンテンツ長を超えるリクエストを拒否します。拒否アクション最大サイズを設定するための追加の機能があります。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
         ctx.getRequest().getBody().then(data -> ctx.render("hello: "+data.getText()));
      })
      .test(httpClient -> {
        ReceivedResponse response = httpClient.request(req->{
          req.method("POST");
          req.getBody().text("world");
        });
        assertEquals("hello: world", response.getBody().getText());
      });
  }
}

3.3.7 バイトチャンクストリーム

Request.getBodyStream()は、受信した個々のチャンクのストリームを返します。

このメソッドは、サーバーで構成された最大コンテンツ長を超えるリクエストを拒否します。最大サイズを設定するための追加の機能があります。

リクエスト本文をファイルにストリーミングする方法の例については、javadocsを参照してください。

4.7 レスポンスの送信

RatpackでのHTTPレスポンスの送信は、簡単、効率的で、柔軟性があります。Ratpackのほとんどのものと同様に、クライアントへのレスポンスの送信はノンブロッキングで行われます。Ratpackはレスポンスを送信するためのいくつかのメカニズムを提供します。レスポンスを操作するためのメソッドは、ResponseオブジェクトとContextオブジェクトにあります。

1.4.7 レスポンスステータスの設定

レスポンスのステータスを設定するには、Response#status(int)またはResponse#status(ratpack.core.http.Status)を呼び出すだけです。

import ratpack.core.http.Response;
import ratpack.core.http.Status;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandlers(chain -> chain
      .all(ctx -> ctx.getResponse().status(202).send("foo"))
    )
    .test(httpClient ->
      assertEquals(202, httpClient.get().getStatusCode())
    );
  }
}

2.4.7 レスポンスの送信

クライアントにレスポンス本文を送信するには、いくつかの方法があります。

Response#send()を呼び出すだけで、レスポンス本文のないレスポンスを送信できます。これが最も短い方法です。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.getResponse().send())
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals("", response.getBody().getText());
      });
  }
}

プレーンテキストレスポンスを送信する場合は、Response#send(String)を使用できます。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.getResponse().send("Ratpack is rad"))
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertTrue(response.getHeaders().get("Content-type").startsWith("text/plain;"));
        assertEquals("Ratpack is rad", response.getBody().getText());
      });
  }
}

レスポンス本文のペイロード(つまり、Stringbyte[]ByteBuf)を送信したり、Content-typeヘッダーを設定したりできる追加のsend()メソッドがあります。レスポンスの送信の詳細については、Responseを参照してください。

3.4.7 レンダラーを使用した代替アプローチ

空のレスポンスや単純なテキストレスポンスを送信しても問題ありませんが、より複雑なレスポンスをクライアントに送信したい場合もあります。Rendererは、特定の型をクライアントにレンダリングできるメカニズムです。より具体的には、コンテキストオブジェクトにあるrender(Object)メソッドを駆動する基礎となるメカニズムです。

次の例では、コンテキストのrender(Object)メソッドを使用して、String型のオブジェクトをレンダリングします。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> ctx.render("Sent using render(Object)!"))
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals("Sent using render(Object)!", response.getBody().getText());
      });
  }
}

StringCharSequence型であるため、RatpackはCharSequenceRendererを見つけて使用してStringをレンダリングします。このCharSequenceRendererはどこから来たのでしょうか?Ratpackは、CharSequenceRendererRenderableRendererPromiseRendererDefaultFileRendererなど、多くのRendererをすぐに提供します。

登録されていない型をレンダリングしようとすると、サーバーエラーが発生します。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  static class Foo {
    public String value;
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        Foo foo = new Foo();
        foo.value = "bar";
        ctx.render(foo);
      })
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals(500, response.getStatusCode());
      });
  }
}

独自のRendererを実装する場合は、Ratpackは独自のRendererを簡単に実装できるRendererSupportを提供します。Ratpackで使用できるように、Rendererを登録することも忘れないでください。

import ratpack.core.handling.Context;
import ratpack.exec.registry.Registry;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.core.render.RendererSupport;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  static class Foo {
    public String value;
  }

  static class FooRenderer extends RendererSupport<Foo> {
    @Override
    public void render(Context ctx, Foo foo) throws Exception {
      ctx.getResponse().send("Custom type: Foo, value=" + foo.value);
    }
  }

  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandlers(chain -> chain
        .register(Registry.single(new FooRenderer()))
        .all(ctx -> {
          Foo foo = new Foo();
          foo.value = "bar";
          ctx.render(foo);
        })
      )
      .test(httpClient -> {
        ReceivedResponse response = httpClient.get();
        assertEquals(200, response.getStatusCode());
        assertEquals("Custom type: Foo, value=bar", response.getBody().getText());
      });
  }
}

4.4.7 JSONの送信

任意のオブジェクトをJSONとしてレンダリングするためのサポートは、Jacksonに基づいています。Jackson renderingに例があります。

5.4.7 ファイルの送信

ファイルなどの静的リソースの送信は、sendFile(Path)で行うことができます。

TODO sendFileメソッドの紹介(代わりにrender(file(«path»))の使用を指摘)

TODO assetsメソッドの紹介

6.4.7 送信前

Responseオブジェクトには、レスポンスがクライアントに送信される直前に呼び出されるbeforeSend(Action<? super Response> responseFinalizer)メソッドが含まれています。

responseFinalizerコールバック内でResponse.send()メソッドを呼び出すことはできません。

このメソッドは、特に以下の変更に役立ちます。

最終的な変更。

StreamedResponse.forwardTo()を使用する場合、ステータスコードまたはヘッダーを変更する実用的なユースケースです。

例:

import io.netty.handler.codec.http.HttpHeaderNames;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.Headers;
import ratpack.core.http.Request;
import ratpack.core.http.Status;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        ctx.getResponse()
          .contentType("application/json")
          .status(Status.OK)
          .beforeSend(response -> {
             response.getHeaders().remove(HttpHeaderNames.CONTENT_LENGTH);
             response.cookie("DNT", "1");
             response.status(Status.of(451, "Unavailable for Legal Reasons"));
             response.contentType("text/plain");
          }).send();
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient.get();

        Headers headers = receivedResponse.getHeaders();
        assertEquals(451, receivedResponse.getStatusCode());
        assertEquals("text/plain", headers.get(HttpHeaderNames.CONTENT_TYPE));
        assertTrue(headers.get(HttpHeaderNames.SET_COOKIE).contains("DNT"));
      });
  }
}

5.7 ヘッダー

HTTPヘッダー情報は、受信リクエストと送信レスポンスの両方で利用できます。

1.5.7 リクエストヘッダー

Headersインターフェースを使用すると、受信リクエストに関連付けられたヘッダー情報を取得できます。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;
import ratpack.core.http.Headers;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        Headers headers = ctx.getRequest().getHeaders();
        String clientHeader = headers.get("Client-Header");
        ctx.getResponse().send(clientHeader);
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient
          .requestSpec(requestSpec ->
              requestSpec.getHeaders().set("Client-Header", "From Client")
          ).get();

        assertEquals("From Client", receivedResponse.getBody().getText());
      });
  }
}

2.5.7 レスポンスヘッダー

MutableHeadersは、レスポンスオブジェクトResponse#getHeaders()を介してレスポンスヘッダーを操作できる機能を提供します。

import ratpack.core.http.MutableHeaders;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp
      .fromHandler(ctx -> {
        MutableHeaders headers = ctx.getResponse().getHeaders();
        headers.add("Custom-Header", "custom-header-value");
        ctx.getResponse().send("ok");
      })
      .test(httpClient -> {
        ReceivedResponse receivedResponse = httpClient.get();
        assertEquals("custom-header-value", receivedResponse.getHeaders().get("Custom-Header"));
      });
  }
}

さらに、set(CharSequence, Object)remove(CharSequence)clear()などを実行できます。

その他のメソッドについては、MutableHeadersを参照してください。

6.7 クッキー

HTTPヘッダーと同様に、クッキーはインバウンドリクエストからの検査と、アウトバウンドレスポンスの操作の両方で利用できます。

1.6.7 インバウンドリクエストからのクッキー

クッキーの値を取得するには、Request#oneCookie(String) を使用できます。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      String username = ctx.getRequest().oneCookie("username");
      ctx.getResponse().send("Welcome to Ratpack, " + username + "!");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
          .getHeaders()
          .set("Cookie", "username=hbogart1"))
        .get();

      assertEquals("Welcome to Ratpack, hbogart1!", response.getBody().getText());
    });
  }
}

Request#getCookies() を使用して、クッキーのセットを取得することもできます。

import io.netty.handler.codec.http.cookie.Cookie;
import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import java.util.Set;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      Set<Cookie> cookies = ctx.getRequest().getCookies();
      assertEquals(1, cookies.size());
      Cookie cookie = cookies.iterator().next();
      assertEquals("username", cookie.name());
      assertEquals("hbogart1", cookie.value());
      ctx.getResponse().send("Welcome to Ratpack, " + cookie.value() + "!");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
          .getHeaders()
          .set("Cookie", "username=hbogart1"))
        .get();

      assertEquals("Welcome to Ratpack, hbogart1!", response.getBody().getText());
    });
  }
}

2.6.7 アウトバウンドレスポンスに対するクッキーの設定

Response#cookie(String, String) を使用して、レスポンスと一緒に送信されるクッキーを設定できます。レスポンスとともに設定されるクッキーのセットを取得するには、Response#getCookies() を使用できます。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      assertTrue(ctx.getResponse().getCookies().isEmpty());
      ctx.getResponse().cookie("whiskey", "make-it-rye");
      assertEquals(1, ctx.getResponse().getCookies().size());
      ctx.getResponse().send("ok");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient.get();
      assertEquals("whiskey=make-it-rye", response.getHeaders().get("Set-Cookie"));
    });
  }
}

クッキーの有効期限を切らせたい場合は、Response#expireCookie() を使用できます。

import ratpack.core.http.client.ReceivedResponse;
import ratpack.test.embed.EmbeddedApp;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx -> {
      ctx.getResponse().expireCookie("username");
      ctx.getResponse().send("ok");
    }).test(httpClient -> {
      ReceivedResponse response = httpClient
        .requestSpec(requestSpec -> requestSpec
            .getHeaders().set("Cookie", "username=lbacall1")
        )
        .get();

      String setCookie = response.getHeaders().get("Set-Cookie");
      assertTrue(setCookie.startsWith("username=; Max-Age=0"));
    });
  }
}

7.7 コンテンツネゴシエーション

リソースの異なる表現(JSON/XML/HTML、GIF/PNGなど)のレンダリングのサポートは、byContent(Action) を介して提供されます。

8.7 セッション

同じクライアントによる複数回の呼び出し間の一貫性を維持する必要がある場合(つまり、ステートフルな操作の場合)、セッションを使用することを検討してください。Ratpackはセッション処理を処理するためのモジュールを提供しています。セッションモジュールをプロジェクトに追加して、Ratpack管理セッションの使用を開始できます。

1.8.7 準備

まず、プロジェクトに必要な依存関係を追加する必要があります。Gradleを使用する場合、依存関係にcompile 'io.ratpack:ratpack-session:2.0.0-rc-1'を追加することで依存関係を追加できます。Gradleファイルの使い始めの状態は以下のようになります。

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "io.ratpack:ratpack-gradle:2.0.0-rc-1"
  }
}

apply plugin: "io.ratpack.ratpack-groovy"

repositories {
  mavenCentral()
}

dependencies {
  runtimeOnly 'org.slf4j:slf4j-simple:1.7.36'
  implementation group: 'io.ratpack', name: 'ratpack-session', version: '2.0.0-rc-1'

  testImplementation "org.spockframework:spock-core:2.1-groovy-3.0"
}

Ratpackでモジュールをロードすることを忘れないでください。

import static ratpack.groovy.Groovy.ratpack
import ratpack.session.SessionModule

ratpack {
	bindings {
		module(SessionModule)
	}
	/* ... */
}

2.8.7 セッションの使用

これでセッションの準備が整いました。以下は、セッションを使用するアプリケーションの簡単な例です。

import ratpack.session.Session

import static ratpack.groovy.Groovy.ratpack
import ratpack.session.SessionModule

ratpack {
	bindings {
		module(SessionModule)
	}

	handlers {
		get('start') { Session session ->
			session.terminate().flatMap {
				session.set('keyForDataStoredInSession', 'Hello Session!').promise()
			}.then {
				response.send('I started a session for you.')
			}
		}
		get { Session session ->
			session.get('keyForDataStoredInSession').then {
				if (it.present) {
					response.send("Message: ${it.get()}")
				} else {
					response.send('I have nothing to say to you')
				}
			}
		}
		get('stop') { Session session ->
			session.terminate().then {
				response.send('The session is dead, dead, dead.')
			}
		}
	}
}

すべてのセッション操作は、Promise または Operation のいずれかを返します。これらを、示されているように変換フローで使用できます。

新しいセッションを開始する前に、古いセッションを終了するようにしてください(get('start')ハンドラーを参照)。このようにして、実際に新しいセッションを取得し、既存のセッションにデータを追加するだけではないことを確認できます。

3.8.7 制限事項

Ratpackセッションモジュールは、デフォルトでインメモリセッションを使用します。一度に最大1000個のセッションを保持でき、新しいセッションが開かれると、最も古いセッションが削除されます。1000個を超えるセッションを想定している場合は、デフォルトモジュールとは異なるセッションストアを使用することを検討する必要があります。たとえば、Redisサーバーが利用可能な場合は、ratpack-session-redisモジュールを使用できます。ペイロードが小さい場合は、クライアントサイドセッションモジュール を使用して、セッションデータの一部をクッキー自体に保存するという別のオプションもあります。ウェブサイト(ドメイン)のすべてのクッキーを合わせた合計は4KBを超えることができないため、ペイロードは小さくする必要があります。

4.8.7 ratpack-session-redisモジュール

Redisセッションモジュールを使用するには、プロジェクトに依存関係(compile 'io.ratpack:ratpack-session-redis:2.0.0-rc-1')を追加します。

次に、セッションモジュールをロードした後にRedisを構成します。

bindings {
  module(SessionModule)
  RedisSessionModule redisSessionModule = new RedisSessionModule()
  redisSessionModule.configure {
    it.host = 'localhost'
    it.port =  6379
    it.password = 'secret'
  }
  module(redisSessionModule)
}

Guiceまたはその他のメカニズムを使用してRedisの構成を注入することをお勧めします。それとは別に、セッションの保存にRedisを使用するようにRatpackを正常に構成しました。Redisサーバーが実行中で利用可能であることを確認し、Ratpackアプリケーションを実行してください。

5.8.7 コメントと便利なリンク

これで、Ratpackでセッションを実装する方法の概要がつかめました。

Ratpackは、UUIDを含むJSESSIONIDクッキーというクッキーを使用してセッションの一貫性を処理します。セッションに最初にデータを追加するときにIDが生成され、レスポンスのAdd-Cookieヘッダーを使用してクッキーがクライアントに送信されます。したがって、クライアントにレスポンスが送信されるようにする必要があります。そうでないと、セッションが失われます。連続するクライアントリクエストには、Cookieヘッダーにクッキーが含まれているため、Ratpackはクライアントのセッションを判断できます。

Ratpackセッション処理の詳細については、以下の便利なリンクを参照してください。- ratpack-sessionモジュールのテスト でセッションの動作方法に関するサンプルの詳細- RatpackのJavadoc には、多くの例と情報が含まれています- クッキーの動作はカスタマイズできます(つまり、クッキーの名前の変更、カスタムID/有効期限の使用)。カスタムSessionCookieConfigをセッションモジュールに提供することで可能です。

6.8.7 デフォルトのセッションストア(インメモリ)を使用する場合の最終的な注意:

デフォルトのセッションストアは、本番環境ではおそらく役に立ちませんが、セッションを使用したローカルテストには役立ちます。session.terminate()の呼び出しにより、セッションクッキーが空の値に設定されます。したがって、連続する呼び出しには、JSESSIONID=のようなクッキーが含まれます。少なくとも、インメモリセッションストアは空の値を有効なセッションとして受け入れます。したがって、値を追加することで新しいセッションを作成しようとする前にセッションを終了しない場合、空のUUIDを持つ既存のセッションにセッションデータを追加することになります。