7 基本HTTP
この章では、リクエストの解析、レスポンスのレンダリング、コンテンツネゴシエーション、ファイルアップロードなど、基本的なHTTPに関する処理方法について説明します。
1.7 リクエストとレスポンス
ハンドラーが操作するコンテキストオブジェクトは、getRequest()
とgetResponse()
メソッドを提供し、それぞれRequest
とResponse
にアクセスできます。これらのオブジェクトは、ほぼ期待通りの機能を提供します。
例えば、両方ともgetHeaders()
メソッドを提供し、リクエストと共に送信されたHTTPヘッダーのモデルと、レスポンスと共に送信されるHTTPヘッダーのモデルを返します。Request
は、HTTPメソッド、URI、クエリ文字列パラメーターのキーバリューモデルなどのその他のメタデータ属性を公開します。
2.7 リダイレクト
redirect(int, Object)
コンテキストメソッドは、リダイレクトの実行をサポートします。このメソッドは、コンテキストレジストリからRedirector
を取得し、引数を転送します。
Ratpackは、以下をサポートするデフォルトの実装を提供します。
- リテラルURL値
- プロトコル相対URL値
- 現在のアプリケーション内の絶対パス
- 現在のアプリケーション内の相対パス
ほとんどのアプリケーションでは、デフォルトの動作で十分なため、カスタム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 …
});
}
}
Form
とUploadedFile
で、詳細と例を参照してください。
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());
});
}
}
レスポンス本文のペイロード(つまり、String
、byte[]
、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());
});
}
}
String
はCharSequence
型であるため、RatpackはCharSequenceRenderer
を見つけて使用してString
をレンダリングします。このCharSequenceRenderer
はどこから来たのでしょうか?Ratpackは、CharSequenceRenderer
、RenderableRenderer
、PromiseRenderer
、DefaultFileRenderer
など、多くの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
を参照してください。
クッキー
HTTPヘッダーと同様に、クッキーはインバウンドリクエストからの検査と、アウトバウンドレスポンスの操作の両方で利用できます。
インバウンドリクエストからのクッキー
クッキーの値を取得するには、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());
});
}
}
アウトバウンドレスポンスに対するクッキーの設定
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を持つ既存のセッションにセッションデータを追加することになります。