Undertow の HttpHandler について

JBossのサポートエンジニアをしている三浦です。

赤帽エンジニアAdvent Calendar 2018の19日目の記事です。今日は Undertow の HttpHandler について書いてみたいと思います。

Undertow とは

Undertow とは、OSS の Java EE アプリケーションサーバである WildFly (商用サポートのあるEnterprise 版は JBoss EAP 7) に組み込まれている Web サーバー実装です。 なお、以前の JBoss AS7/JBoss EAP 6 までは、JBossWeb という Tomcat をベースにした実装が組み込まれていましたが、WildFly 8/JBoss EAP 7以降から、この Undertow に置き換わっています。

公式サイト にある Undertow の特徴をいくつか抜粋します。

  • Java NIOベースのWebサーバー実装で、ブロッキングI/OとノンブロッキングI/Oの両方をサポートする
  • Builder API を利用することで Java アプリケーションに簡単に組み込むことができる
  • HttpHandler を組み合わせることで、簡単なWebサーバーから Java EE Servlet 4 対応のWebコンテナまで実装できる柔軟性がある
  • HTTP/2 対応 (JDK 8 環境でも jetty ALPN jar の boot class path への追加なしでOK)
  • Servlet 4.0 対応

HttpHandler とは

Undertow でのリクエスト処理は HttpHandler(以下、ハンドラー)) というもので実装することができ、上の特徴で述べられているように複数のハンドラーを組み合わせるすることでリクエスト処理の機能拡張ができるようになっています。Undertow が提供する多くの機能もハンドラーとして実装されています。

公式ドキュメントUndertow Handler Authors Guide などに実装方法が解説されています。ただ、こちらは Undertow のドキュメントなので、主に Java アプリケーションへの組み込む場合についての例となっており、WildFly 上で動かしてみる具体的な例はありません。

Enterprise版である JBoss EAP 7 の Development Guide に少し記載はありますし、Undertow 内の実装を参考にすることはできるのですが、まずは、シンプルな実装のハンドラーを作成して、WildFly 15 に設定して動かしてみるまでをやってみましょう。

Undertow HttpHandler を作ってみる

まずは、pom.xml に必要な maven の依存定義です。

シンプルな HttpHandler を実装するだけであれば、必要になるのは io.undertow:undertow-core のみです。今回の例ではロギングライブラリとして WildFly に組み込まれている jboss-logging を利用したので、その依存定義も追加していますが、java.util.logging を使うなら不要ですし、他のロギングライブラリを使うのであれば適宜変更してください。どちらも WildFly に含まれているので <scope>provided でOKです。

    <dependencies>
        <dependency>
            <groupId>io.undertow</groupId>
            <artifactId>undertow-core</artifactId>
            <version>${version.io.undertow}</version> <!-- 2.0.15.Final が WildFly 15 に同梱 -->
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
            <version>${version.org.jboss.logging}</version> <!-- 3.3.2.Final が WildFly 15 に同梱 -->
            <scope>provided</scope>
        </dependency>
    </dependencies>

ハンドラー実装クラスとして実装する必要があるのは、io.undertow.server.HttpHandler のみです。

public interface HttpHandler {
    void handleRequest(HttpServerExchange exchange) throws Exception;
}

通常、ハンドラーは次のハンドラーに処理をチェインしていくので、以下のように処理を記載した最後に next.handleRequest(exchange) のように書き、次のハンドラーに処理を渡します。

package org.jboss.example.undertow;

import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.jboss.logging.Logger;

public class HelloHandler implements HttpHandler {
    private final Logger log = Logger.getLogger(HelloHandler.class);
    private HttpHandler next;

    public HelloHandler(HttpHandler next) {
        this.next = next;
    }

    @Override
    public void handleRequest(HttpServerExchange exchange) throws Exception {
        log.info("handleRequest() is invoked.");
        // ここにリクエスト処理を記載する
        next.handleRequest(exchange);
    }
}

リクエスト処理は、HttpServerExchange というリクエスト/レスポンスの情報を格納するオブジェクト経由で処理を行うことができます。

HttpServerExchange には多くの API があるので、すべては紹介できませんが、たとえば、リクエスト・レスポンスのヘッダー情報は HttpServerExchange#getRequestHeaders() / HttpServerExchange#getResponseHeaders() から HeaderMap として取得・操作ができ、HttpServerExchange#getResponseSender() からレスポンスを書き込む Sender が取得できます。

もし、次のハンドラーに処理を渡さず、このハンドラーでレスポンスを返してリクエスト処理を終了するのであれば、以下のように next.handleRequest(exchange) を呼ばずに終了することもできます。

import ...(上と同じなので省略)...
import io.undertow.util.Headers; // 追加

...(省略)...

    public void handleRequest(final HttpServerExchange exchange) throws Exception {
        log.info("handleRequest() is invoked.");
        log.info("requestPath = " + exchange.getRequestPath());
        exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
        exchange.getResponseSender().send("Hello, World");
    }

ここまでの実装で WildFly で動かしてみましょう。

ハンドラーを WildFly に追加して動かしてみる

アプリケーション内にパッケージングして WEB-INF/jboss-web.xml に定義して利用する方法もありますが、今回はまだアプリケーションは特にデプロイしていないので、WildFly のカスタムモジュールとして配置して利用したいと思います。

WildFly をダウンロードして、起動します。

$ curl -L https://download.jboss.org/wildfly/15.0.0.Final/wildfly-15.0.0.Final.tar.gz | tar xz
$ cd wildfly-15.0.0.Final/
$ ./bin/standalone.sh

wildfly-maven-plugin を使うと、 pom.xml に以下のような定義を追加することで <commands> に指定したモジュール登録のCLIコマンドが wildfly:execute-commands で実行できるようになるので追加します。

<build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
        <plugin>
            <groupId>org.wildfly.plugins</groupId>
            <artifactId>wildfly-maven-plugin</artifactId>
            <version>${version.wildfly.maven.plugin}</version> <!-- 現時点の最新は 2.0.0.Final -->
            <executions>
            </executions>
            <configuration>
                <!-- Tells plugin to start in offline mode, to not try to connect to server or start it-->
                <offline>true</offline>
                <fork>true</fork>
                <jboss-home>${wildfly.home.dir}</jboss-home>
                <!-- 実行するCLIを外だしにすることも可能 -->
                <!-- <scripts> -->
                <!--     <script>config.cli</script> -->
                <!-- </scripts> -->
                <commands>
                    <command>module add --name=org.jboss.example.undertow --resources=${project.build.directory}${file.separator}${project.artifactId}.jar --dependencies=io.undertow.core,org.jboss.logging,javaee.api,javax.api</command>
                </commands>
            </configuration>
        </plugin>
    </plugins>
</build>

それが終わったら、以下の mvn コマンドで、作成したハンドラーをビルドしてモジュールとして登録します。

$ mvn clean package wildfly:execute-commands -Dwildfly.home.dir=/path/to/wildfly-15.0.0.Final 

もちろん、 wildfly:execute-commands を使わずに、jboss-cli.sh から直接CLIを実行して登録してもよいです。

$ cd wildfly-15.0.0.Final/
$ ./bin/jboss-cli.sh -c
[standalone@localhost:9990 /] module add --name=org.jboss.example.undertow --slot=main --resources=/path/to/undertow-example-handler.jar --dependencies=io.undertow.core,org.jboss.logging,javaee.api,javax.api

以下の2つのCLIコマンドでを実行して、配置したハンドラーを undertow サブシステムの filter として有効化します。

/subsystem=undertow/configuration=filter/custom-filter=hello-handler:add(class-name=org.jboss.example.undertow.HelloHandler, module=org.jboss.example.undertow)
/subsystem=undertow/server=default-server/host=default-host/filter-ref=hello-handler:add()

standalone/configuration/standalone.xml には以下のような定義が追加されます。

<subsystem xmlns="urn:jboss:domain:undertow:8.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other">
    <buffer-cache name="default"/>
    <server name="default-server">
        <http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
        <https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
        <host name="default-host" alias="localhost">
            <location name="/" handler="welcome-content"/>
            <filter-ref name="hello-handler"/> <!-- 追加 -->
            ...
        </host>
    </server>
    ...
    <filters>
        <filter name="hello-handler" class-name="org.jboss.example.undertow.HelloHandler" module="org.jboss.example.undertow"/> <!-- 追加 -->
    </filters>
</subsystem>

これでハンドラーが利用可能になり、リクエストを投げてみると、ハンドラーからレスポンスが帰ってくることが確認できます。

$ curl -v http://localhost:8080
...
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain
< Content-Length: 13
< Date: Wed, 19 Dec 2018 10:17:06 GMT
< 
Hello, World!

ハンドラーが動くURIパスを設定で指定する

上の設定例では <filter-ref name="hello-handler"/> と特にパスなどを指定していないため、どんなパスでリクエストしてもハンドラーがレスポンスを返してきます。

たとえば、URIのパスが /hello 以下でのみ動くようにするには、<filter-ref>predicate 属性で path-prefix(/hello) などのようにフィルターが適用される条件を指定します。predicate 属性に指定できる記法の詳細については Undertow のドキュメントの 「Textual Representation of Predicates」のセクション にありますが、path-prefix (パスの前方マッチ) 以外にも、path-suffix (パスの後方マッチ) や regex (正規表現での指定) や method (リクエストのメソッド) などを指定できます。andornot で複数の条件を指定することもできます。

既存 <filter-ref> 定義の更新は、以下のようなCLIを実行することで反映できます。(新規追加時は :reload 不要ですが、 既存定義の更新なので :reload が必要です。)

/subsystem=undertow/server=default-server/host=default-host/filter-ref=hello-handler:write-attribute(name=predicate,value="path-prefix(/hello)")
:reload

standalone.xml の undertow サブシステムの設定は以下のように変わります。

<host name="default-host" alias="localhost">
    ...
    <filter-ref name="hello-handler" predicate="path-prefix(/hello)"/> <!-- 変更される -->
    ...
</host>

これで http://localhost:8080/hello 以外ではハンドラーがレスポンスを返すことがなくなりました。

ハンドラーの処理を task スレッド上で動かす

ここで standalone/log/server.log に出力されるログをみてみると、リクエスト処理は I/O スレッドで動いていることがわかります。

INFO  [org.jboss.example.undertow.HelloHandler] (default I/O-5) handleRequest() is invoked.
INFO  [org.jboss.example.undertow.HelloHandler] (default I/O-5) requestPath = /hello

ノンブロッキングな処理であれば、特に問題ありませんが、時間がかかったりブロックするような重い処理を実行している場合には、I/Oスレッドではなく、task スレッドという別なワーカースレッドプールで処理を行うようにすべきであると Undertow のドキュメントにも記載されています。

task スレッドにディスパッチするのは簡単で、ハンドラー内で以下のような実装を追加すればよいです。

@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
    if (exchange.isInIoThread()) {
        exchange.dispatch(this);
        return;
    }
    // ブロッキングな処理を行うコード
}

今回の処理は特にブロッキングな処理はないのですが、HelloHandler#handleRequest() を以下のように書き換えてみます。

@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
    if (exchange.isInIoThread()) {
        exchange.dispatch(this);
        log.info("Running on I/O thread. Let's dispatch to task thread");
        return;
    }
    log.info("handleRequest() is invoked.");
    log.info("requestPath = " + exchange.getRequestPath());
    exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
    exchange.getResponseSender().send("Hello, World");
    exchange.endExchange();
}

配置したモジュールは一旦削除してから、新しいモジュールを改めて追加して、再起動します。

$ ./bin/jboss-cli.sh -c
[standalone@localhost:9990 /] module remove --name=org.jboss.example.undertow
[standalone@localhost:9990 /] module add --name=org.jboss.example.undertow --slot=main --resources=/path/to/undertow-example-handler.jar --dependencies=io.undertow.core,org.jboss.logging,javaee.api,javax.api

これで http://localhost:8080/hello にアクセスして、ログから HelloHandler が task スレッドで実行されていることがわかります。

INFO  [org.jboss.example.undertow.HelloHandler] (default I/O-3) Running on I/O thread. Let's dispatch to task thread
INFO  [org.jboss.example.undertow.HelloHandler] (default task-1) handleRequest() is invoked.
INFO  [org.jboss.example.undertow.HelloHandler] (default task-1) requestPath = /hello

ハンドラーに設定パラメータを渡す

また、WildFly 上でハンドラーが定義されているとき、ハンドラーに対応するアクセサーメソッドを用意することで、<filter> 定義のパラメータを渡すこともできます。

設定で message というパラメータを渡して、それをレスポンスとして返すようにしてみます。HelloHandler を以下のように更新します。

public class HelloHandler implements HttpHandler {

    private static final String DEFAULT_MESSAGE = "Hello, World!";
    private final Logger log = Logger.getLogger(HelloHandler.class);

    private HttpHandler next;
    private String message;

    public HelloHandler(HttpHandler next) {
        this.message = DEFAULT_MESSAGE;
        this.next = next;
    }

    public HelloHandler(String message, HttpHandler next) {
        this.message = message;
        this.next = next;
    }

    @Override
    public void handleRequest(final HttpServerExchange exchange) throws Exception {
        if (exchange.isInIoThread()) {
            exchange.dispatch(this);
            log.info("Running on I/O thread. Let's dispatch to task thread");
            return;
        }
        log.info("handleRequest() is invoked.");
        log.info("requestPath = " + exchange.getRequestPath());
        exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
        exchange.getResponseSender().send(message);
        exchange.endExchange();
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

先ほどと同様に、配置したモジュールは一旦削除してから、新しいモジュールを改めて追加して、再起動します。

そして、以下のような CLI で <filter name="hello-handler" ...>message パラメータをセットします。

/subsystem=undertow/configuration=filter/custom-filter=hello-handler:write-attribute(name=parameters.message,value="Hello, WildFly")

standalone.xml の undertow サブシステムの設定は以下のように変わります。

<filter name="example-handler" class-name="org.jboss.example.undertow.HelloHandler" module="org.jboss.example.undertow">
    <param name="message" value="Hello, WildFly"/> <!-- 追加される -->
</filter>

これで http://localhost:8080/hello にアクセスしてハンドラーが返すレスポンスも変わったことが確認できます。

$ curl -v http://localhost:8080/hello
...
> GET /hello HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain
< Content-Length: 14
< Date: Wed, 19 Dec 2018 11:30:43 GMT
< 
* Connection #0 to host localhost left intact
Hello, WildFly

細かく説明したのですこし長くなりましたが、シンプルな Undertow HttpHandler の作り方と WildFly 15 での設定方法でした。WildFly 15 / Undertow 2.0.x を例に書きましたが、ここで書いた内容は EAP 7.0.x/Undertow 1.3.x や EAP 7.1.x/Undertow 1.4.x でも変わらないと思います。

作ったコード:

github.com

Undertow の built-in handler を紹介する

独自 HttpHandler の作り方と設定の仕方はわかったけど、この例だとシンプル過ぎて実用性がないし、あまりうれしさがないですね。

Undertow には built-in Handlers に記載されているように多くのハンドラーが同梱されており、WildFly 上で設定して簡単に利用できるようになっています。

これらの built-in handler は <expression-filter> に指定可能な短縮名が定義されています。<expression-filter>expression 属性で predicates と組み合わせて指定して利用することもできるようになっています。

というわけで、最後に、知っておくと有用そうな built-in handler をいくつか紹介して終わりたいと思います。

Request Dumping Handler

実装クラス io.undertow.server.handlers.RequestDumpingHandler
短縮名 dump-request
指定可能パラメータ なし
デフォルトパラメータ なし

リクエストとレスポンスのヘッダー情報やリクエストパラメータなどをログにダンプするハンドラーです。ログ出力量が多くなるので、predicate を指定して出力対象を限定するなどをすることが多いかもしれません。

  • 設定するCLIコマンドの例:
/subsystem=undertow/configuration=filter/expression-filter=dump-request-handler:add(expression="dump-request")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=dump-request-handler:add
  • standalone.xml での設定例:
<subsystem xmlns="urn:jboss:domain:undertow:8.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other">
    ...
    <server name="default-server">
        ...
        <host name="default-host" alias="localhost">
            ...
            <filter-ref name="dump-request-handler"/> <!-- 追加 -->
            <!-- <filter-ref name="dump-request-handler" predicate="path-prefix(/test)"/> -->
            ...
        </host>
    </server>
    ...
    <filters>
        <expression-filter name="dump-request-handler" expression="dump-request"/> <!-- 追加 -->
    </filters>
</subsystem>

例えば、curl http://localhost:8080/test/?foo=bar -H "TestHeader: test" のようなリクエストを投げると、以下のようなログが出力されます。

INFO  [io.undertow.request.dump] (default task-1) 
----------------------------REQUEST---------------------------
               URI=/test/
 characterEncoding=null
     contentLength=-1
       contentType=null
            header=Accept=*/*
            header=TestHeader=test
            header=User-Agent=curl/7.29.0
            header=Host=localhost:8080
            locale=[]
            method=GET
         parameter=foo=bar
          protocol=HTTP/1.1
       queryString=foo=bar
        remoteAddr=/127.0.0.1:35140
        remoteHost=localhost
            scheme=http
              host=localhost:8080
        serverPort=8080
          isSecure=false
--------------------------RESPONSE--------------------------
     contentLength=874
       contentType=text/html;charset=UTF-8
            cookie=JSESSIONID=LMovDERaWI4EtB2MvkmHj6yfXYoLXwADm3m2szaS.node1; domain=null; path=/test
            header=Connection=keep-alive
            header=X-Powered-By=JSP/2.3
            header=Set-Cookie=JSESSIONID=LMovDERaWI4EtB2MvkmHj6yfXYoLXwADm3m2szaS.node1; path=/test
            header=Content-Type=text/html;charset=UTF-8
            header=Content-Length=874
            header=Date=Wed, 19 Dec 2018 12:46:40 GMT
            status=200

==============================================================

Blocking Handler

実装クラス io.undertow.server.handlers.BlockingHandler
短縮名 blocking
指定可能パラメータ なし
デフォルトパラメータ なし

HttpServerExchange をブロッキングモードして、I/Oスレッド上で動いている場合には task スレッドに処理をディスパッチするハンドラーです。

<filters> に指定されたハンドラーはI/Oスレッド上で動くので、<expression-filter> にて、この BlockingHandler を各種ハンドラーと組み合わせて利用することで、後続のハンドラーを task スレッド上で稼働させることができます。このハンドラーを単体で利用するユースケースはあまりないと思います。

Stuck Thread Detection Handler

実装クラス io.undertow.server.handlers.StuckThreadDetectionHandler
短縮名 stuck-thread-detector
指定可能パラメータ threshhold: int (単位は秒)
デフォルトパラメータ threshhold

threshhold に指定した値(単位は秒)以上に処理に時間がかかっているスレッドをログに出力するハンドラーです。threshhold のデフォルトは 600 (600秒つまり10分) です。

ServletやEJBアプリケーションなどは task ワーカースレッド上で動くため、このハンドラーで検知するには、前述した BlockingHandler と組み合わせて利用する必要があります。

なお、threshhold はデフォルトパラメータなので、stuck-thread-detector(5) と指定しても、stuck-thread-detector(threshhold=5) と指定しても同じです。

  • 設定するCLIコマンドの例:
/subsystem=undertow/configuration=filter/expression-filter=stuck-thread-detector-handler:add(expression="blocking; stuck-thread-detector(5)")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=stuck-thread-detector-handler:add
  • standalone.xml での設定例:
<subsystem xmlns="urn:jboss:domain:undertow:8.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other">
    ...
    <server name="default-server">
        ...
        <host name="default-host" alias="localhost">
            ...
            <filter-ref name="stuck-thread-detector-handler"/> <!-- 追加 -->
            <!-- <filter-ref name="dump-request-handler" predicate="path-prefix(/test)"/> -->
            ...
        </host>
    </server>
    ...
    <filters>
        <expression-filter name="stuck-thread-detector-handler" expression="blocking; stuck-thread-detector(5)"/> <!-- 追加 -->
    </filters>
</subsystem>

上の設定例では、threshhold を5秒に指定しているので5秒以上かかる処理、例えば、10秒スリープするだけのJSPにアクセスすると、5秒超過時に以下のように時間がかかっている処理スタックトレースとともに WARN ログが出力されます。

22:05:27,070 WARN  [io.undertow.request] (default I/O-11) UT005072: Thread default task-1 (id=148) has been active for 5007 milliseconds (since Wed Dec 19 22:05:22 JST 2018) to serve the same request for /test/sleep.jsp and may be stuck (configured threshold for this StuckThreadDetectionValve is 5 seconds). There is/are 1 thread(s) in total that are monitored by this Valve and may be stuck.: java.lang.Throwable
    at java.lang.Thread.sleep(Native Method)
    at org.apache.jsp.sleep_jsp._jspService(sleep_jsp.java:102)
    at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:791)
    at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:433)
    at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:403)
    at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:347)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:791)
    at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.jsp.JspFileHandler.handleRequest(JspFileHandler.java:32)
    at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.NotificationReceiverHandler.handleRequest(NotificationReceiverHandler.java:50)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:292)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:138)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction.lambda$create$0(SecurityContextThreadSetupAction.java:105)
    at org.wildfly.extension.undertow.security.SecurityContextThreadSetupAction$$Lambda$661/801187413.call(Unknown Source)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction$$Lambda$662/1452439737.call(Unknown Source)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction$$Lambda$662/1452439737.call(Unknown Source)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction$$Lambda$662/1452439737.call(Unknown Source)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1502)
    at org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction$$Lambda$662/1452439737.call(Unknown Source)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:272)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleRequest(ServletInitialHandler.java:197)
    at io.undertow.server.handlers.HttpContinueReadHandler.handleRequest(HttpContinueReadHandler.java:65)
    at io.undertow.server.handlers.PathHandler.handleRequest(PathHandler.java:94)
    at org.wildfly.extension.undertow.Host$OptionsHandler.handleRequest(Host.java:386)
    at io.undertow.server.handlers.HttpContinueReadHandler.handleRequest(HttpContinueReadHandler.java:65)
    at io.undertow.predicate.PredicatesHandler.handleRequest(PredicatesHandler.java:110)
    at io.undertow.server.handlers.RequestDumpingHandler.handleRequest(RequestDumpingHandler.java:162)
    at io.undertow.predicate.PredicatesHandler.handleRequest(PredicatesHandler.java:93)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.predicate.PredicatesHandler.handleRequest(PredicatesHandler.java:110)
    at io.undertow.server.handlers.StuckThreadDetectionHandler.handleRequest(StuckThreadDetectionHandler.java:168)
    at io.undertow.predicate.PredicatesHandler.handleRequest(PredicatesHandler.java:93)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:360)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1985)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1487)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1349)
    at java.lang.Thread.run(Thread.java:748)

そして、時間がかかっていた処理が完了した際に、以下のような WARN ログが出力されます。

22:05:33,080 WARN  [io.undertow.request] (default I/O-11) UT005073: Thread default task-1 (id=148) was previously reported to be stuck but has completed. It was active for approximately 10694 milliseconds. There is/are still 0 thread(s) that are monitored by this Valve and may be stuck.

Set Attribute Handler

実装クラス io.undertow.server.handlers.SetAttributeHandler
短縮名 set
指定可能パラメータ attribute: ExchangeAttribute (必須)
value: ExchangeAttribute (必須)
デフォルトパラメータ なし

リクエスト/レスポンスの Exchange Attributes の値を変更するハンドラーです。変更できるのは変更可能な Exchange Attributes に限ります。

ドキュメントの「Exchange Attributes」のセクション に記載があるように、リクエストヘッダーが %{i,request_header_name}、レスポンスヘッダーが %{o,response_header_name}、クエリパラメータが %{q,query_param_name}、 Cookie が %{c,cookie_name} のように指定できます。

リクエストヘッダーの情報は、Servlet filter を利用しても書き換えるといったことはできませんが、このハンドラーを利用することで変更することができます。あまりよい例は浮かびませんが、、問題のある特定のヘッダーやCookieなどをアドホックに書き換えて対応したいという場合などに使えます。

  • 設定するCLIコマンドの例:
/subsystem=undertow/configuration=filter/expression-filter=set-attr-filter:add(expression="set(attribute='%{i,ExampleHeader}', value='newvalue')")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=set-attr-filter:add
  • standalone.xml での設定例:
<subsystem xmlns="urn:jboss:domain:undertow:8.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other">
    ...
    <server name="default-server">
        ...
        <host name="default-host" alias="localhost">
            ...
            <filter-ref name="set-attr-filter"/> <!-- 追加 -->
            <!-- <filter-ref name="set-attr-filter" predicate="path-prefix(/test)"/> -->
            ...
        </host>
    </server>
    ...
    <filters>
        <expression-filter name="set-attr-filter" expression="set(attribute='%{i,ExampleHeader}', value='newvalue')"/>
 <!-- 追加 -->
    </filters>
</subsystem>

Clear Handler

実装クラス io.undertow.server.handlers.SetAttributeHandler
短縮名 clear
指定可能パラメータ attribute: ExchangeAttribute (必須)
デフォルトパラメータ attribute

こちらは実装としては上の SetAttributeHandler と同じですが、違いは attribute の変更ではなく削除を行うものです。

たとえば、これも極端な例ですが、GET リクエストにおいて通常は必要のない Content-Type ヘッダーが付与されており、その結果としてアプリケーション処理に問題が発生しているというケースに、リクエストの Content-Type ヘッダーを削除したいといったケースなどで利用できます。

なお、attribute はデフォルトパラメータなので、 clear(attribute='%{i,Content-Type}') と書いても clear(%{i,Content-Type}) と書いても同じです。

  • 設定するCLIコマンドの例:
/subsystem=undertow/configuration=filter/expression-filter=clear-content-type-header-on-get-filter:add(expression="method(GET) and regex(pattern='text/plain.*', value='%{i,Content-Type}') -> clear(attribute='%{i,Content-Type}')")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=clear-content-type-header-on-get-filter:add
  • standalone.xml での設定例:
<subsystem xmlns="urn:jboss:domain:undertow:8.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other">
    ...
    <server name="default-server">
        ...
        <host name="default-host" alias="localhost">
            ...
            <filter-ref name="clear-content-type-header-on-get-filter"/> <!-- 追加 -->
            <!-- <filter-ref name="clear-content-type-header-on-get-filter" predicate="path-prefix(/test)"/> -->
            ...
        </host>
    </server>
    ...
    <filters>
        <expression-filter name="clear-content-type-header-on-get-filter" expression="method(GET) and regex(pattern='text/plain.*', value='%{i,Content-Type}') -> clear(attribute='%{i,Content-Type}')"/>
 <!-- 追加 -->
    </filters>
</subsystem>

Proxy Peer Address Handler

実装クラス io.undertow.server.handlers.ProxyPeerAddressHandler
短縮名 proxy-peer-address
指定可能パラメータ なし
デフォルトパラメータ なし

前段のロードバランサーやプロキシがリクエストに付与した X-Forwarded-* ヘッダー (X-Forwarded-ForX-Forwarded-ProtoX-Forwarded-HostX-Forwarded-Port) の情報に基づいて、リクエストの情報(Hostヘッダー、スキーム、受付ポートなど)を更新するハンドラーです。 これによってリダイレクト時の Location ヘッダーが送られてきた X-Forwarded-* ヘッダーに基づいて組み立てられるようになり、適切なURLにリダイレクトされるようになるというものです。 ただ、

設定例などは省略します。

Forwarded Handler

実装クラス io.undertow.server.handlers.ForwardedHandler
短縮名 proxy-peer-address
指定可能パラメータ なし
デフォルトパラメータ なし

やっていることは ProxyPeerAddressHandler と同じですが、 RFC7239 で定義された Forwarded ヘッダーに基づいてリクエストの情報(Hostヘッダー、スキーム、受付ポートなど)を更新するハンドラーです。

ロードバランサーやプロキシーが RFC7239 に準拠したヘッダーを送ってくるかどうか次第です。また、通常は ProxyPeerAddressHandler と ForwardedHandler の両方を利用するのではなく、どちらかを利用すればよいでしょう。

ただ、 X-Forwarded-* ヘッダーおよびForwarded ヘッダーを意図的に改竄して送ることは簡単にできてしまうので、どちらも前段にロードバランサーやプロキシーがいる場合にのみ利用すべきものです、

設定例などは省略します。

Redirect Handler

実装クラス io.undertow.server.handlers.RedirectHandler
短縮名 redirect
指定可能パラメータ value: ExchangeAttribute (必須)
デフォルトパラメータ value

指定したURLに 302 Redirect レスポンスを返すハンドラーです。ステータスコードは 302 で固定です。

以下の設定例だと、/test 以外のURIパスへのアクセスだった場合に、http://localhost/test/index.html に 302 リダイレクトを返すことができます。

  • 設定するCLIコマンドの例:
/subsystem=undertow/configuration=filter/expression-filter=redirect-handler:add(expression="redirect(http://localhost:8080/test/)")
/subsystem=undertow/server=default-server/host=default-host/filter-ref=redirect-handler:add(predicate="not path-prefix(/test)")
  • standalone.xml での設定例:
<subsystem xmlns="urn:jboss:domain:undertow:8.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other">
    ...
    <server name="default-server">
        ...
        <host name="default-host" alias="localhost">
            ...
            <filter-ref name="redirect-handler" predicate="not path-prefix(/test)"/> <!-- 追加 -->
            ...
        </host>
    </server>
    ...
    <filters>
        <expression-filter name="redirect-handler" expression="redirect(http://localhost/test/index.html)"/>
 <!-- 追加 -->
    </filters>
</subsystem>

Response Code Handler

実装クラス io.undertow.server.handlers.ResponseCodeHandler
短縮名 response-code
指定可能パラメータ value: int (必須)
デフォルトパラメータ value

指定したレスポンスコードを返すハンドラーです。

例えば、 response-code(403) と指定すれば 403 Forbidden とともにデフォルトのエラーページが返却されます。

また、RedirectHandler では 302 固定でしたが、この ResponseCodeHandler と前述の SetAttributeHandler を組み合わせて、Location ヘッダーをセットしてから response-code(301) と指定することで、301 Moved Permanently でリダイレクトさせることもできたりします。

CLIは省略しますが、standalone.xml での設定例です。

<expression-filter name="301-redirect-filter" expression="set(attribute='%{o,Location}', value='http://www.example.com'); response-code(301)"/>

Disable Cache Handler

実装クラス io.undertow.server.handlers.DisableCacheHandler
短縮名 disable-cache
指定可能パラメータ なし
デフォルトパラメータ なし

レスポンスに以下のヘッダーを付与するハンドラーです。設定例などは省略します。

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。