今日ちょっとハマってURLについて少しだけ調べたんですが、java.net.URI
はRFC 3986ではなくRFC 2396準拠なんですね。そして、ハマった原因はRFC 2396とRFC 3986の違いにあったのです
- http://www.ietf.org/rfc/rfc2396.txt
- http://www.ietf.org/rfc/rfc3986.txt
- https://docs.oracle.com/javase/jp/8/docs/api/java/net/URI.html
環境
- 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つかったことなくて、実は今日初めて知ったのですが・・・ )
設定的には・・・以下のような感じになってました。(関係ないところは省いてます+実際はlocalhostじゃありません)
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のバグでした・・・