LoginSignup
14
12

More than 5 years have passed since last update.

java.net.URIで扱えるホスト名はRFC 2396 (RFC 3986じゃない・・)

Last updated at Posted at 2016-05-24

今日ちょっとハマってURLについて少しだけ調べたんですが、java.net.URIはRFC 3986ではなくRFC 2396準拠なんですね。そして、ハマった原因はRFC 2396とRFC 3986の違いにあったのです :weary:

環境

  • Java SE 8
  • Spring Boot 1.3.5.RELEASE (Tomcat 8.0)
  • Nginx 1.9.?

違いは・・・

細かい違いはRFCをみていただくとして・・・わたしがハマったのはホスト名のところです。
RFC 2396では、

host          = hostname | IPv4address
# 以降、IPアドレス系の定義は省略します
hostname      = *( domainlabel "." ) toplabel [ "." ]
domainlabel   = alphanum | alphanum *( alphanum | "-" ) alphanum
toplabel      = alpha | alpha *( alphanum | "-" ) alphanum

(「半角英数字」「"-"」「"."」のみ)だったのが、RFC 3986では、

host        = IP-literal / IPv4address / reg-name
# 以降、IPアドレス系の定義は省略します
reg-name    = *( unreserved / pct-encoded / sub-delims )
unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
                  / "*" / "+" / "," / ";" / "="

になっています。だいぶ扱える文字が増えています。

ハマったこと・・・

いま携わっている案件では、Webサーバが「Nginx」、アプリがSpring Boot(組み込みTomcat)という構成になっていて、NginxからSpring Bootにリクエストをプロキシする際に、upstreamディレクティブを使っていました。(Nginxつかったことなくて、実は今日初めて知ったのですが・・・ :sweat_smile:
設定的には・・・以下のような感じになってました。(関係ないところは省いてます+実際はlocalhostじゃありません)

nginx.conf
http {
    upstream spring_boot {
        server localhost:8080;
    }
    server {
        listen       18080;
        server_name  localhost;
        location /spring-boot/ {
            proxy_pass http://spring_boot/;
        }
    }
}

この状態でNginxにリクエスト(http://localhost:18080/spring-boot/)を送ると、Spring Boot側にプロキシされるリクエスト(http://localhost:8080/)のHostヘッダーは「spring_boot」(upstream名)になります。Servlet APIのHttpServletRequest#getRequestURLメソッドは、Hostヘッダーを参照してURLを返却するため、このケースだと「http://spring_boot」(RFC 2396でホスト名として扱えない文字「_」がある状態・・・)になるわけです。

で、実際にハマったところは・・・
Spring Frameworkはorg.springframework.web.util.UriComponentsBuilderというURLを扱うユーティリティを提供しており、そのクラスのfromHttpRequestメソッドの処理中でHttpServletRequest#getRequestURLメソッドから取得したURL文字列を指定してjava.net.URIのインスタンスを作成しています。結果としてどうなるかというと、URI#getHost()からnullが返却されるため、ホスト名を取得することができません。

原因は・・・

Java標準のURIクラスで扱えない文字が含まれるURLを渡していることが直接的な原因ですが、おおもとの原因はNginx側の設定でした。ただし・・・Spring Framework側に全く問題がないかというと、必ずしもそうではないかも・・・。同じクラスにある「URI文字列を渡すメソッド(UriComponentsBuilder#fromUriString(String) or fromHttpUrl())」では、RFC 3986をサポートしているようなので・・・・。+Servlet API自体はjava.net.URIに依存していないので、Spring Frameworkの実装がこのエラーを生んでいる気もするな。。。

対策は・・・

まず「ホスト名はRFC 2396に準拠する!!!」 これが一番大事。upstream名もRFC 2396に準拠しておくのが無難。

そして・・・
Hostヘッダがupstreamの名前になるのは基本ダメだと思われるので、Nginxにアクセスする際に指定するホスト名を引き継ぐようにするのがよさげな気がしています。HostヘッダーにNginxにアクセスする際に指定したホスト名を引き継ぐ場合は、以下のようにproxy_set_headerディレクティブを使います。

location /spring-boot/ {
    proxy_pass http://spring-boot/;
    proxy_set_header host $host;
}

なお、UriComponentsBuilder#fromHttpRequest(HttpRequest)は、「Forwarded」や「X-Forwarded-Host」「X-Forwarded-Port」「X-Forwarded-Proto」ヘッダがあるとそちらからスキーマ、ホスト名、ポート番号を取得する仕組みになっているので、これらのヘッダを活用する方法もあります。

既知の発生条件は・・・

この事象は必ず発生するわけではありません。既知の発生条件は以下のとおりです。

  • リクエストにOriginヘッダーがあるとCORS(Cross-Origin Resource Sharing)関連の処理が動き、その処理の中でUriComponentsBuilder#fromHttpRequestメソッドを使用しており、後続処理でホスト名を参照しているところでNullpointerExceptionが発生します。
スタックトレース(抜粋)
...
2016-05-25 04:46:48.890 ERROR 84097 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause

java.lang.NullPointerException: null
    at org.springframework.web.util.WebUtils.isSameOrigin(WebUtils.java:816) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.cors.DefaultCorsProcessor.processRequest(DefaultCorsProcessor.java:76) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.handler.AbstractHandlerMapping$CorsInterceptor.preHandle(AbstractHandlerMapping.java:503) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:134) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:956) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:858) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[tomcat-embed-core-8.0.33.jar:8.0.33]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
...

まとめ

特にまとめることはありませんが、Nginx + Spring Boot(Spring MVC)を組み合わせる場合は、今回投稿した内容を頭の片隅にいれておくとよいかもしれません。

補足

2016/5/26
CORS(Cross-Origin Resource Sharing)関連の処理でエラーになる件は、Spring Frameworkのバグでした・・・ :sweat_smile:

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12