Spring Webflux出現http 413問題的解決過程
技術標籤:spring webfluxjava
今天收到一封報警郵件,發現有一個介面已經連續一週穩定性不達標,so趕緊登陸跳板機檢視。
查了下報警介面所在的服務的日誌,沒發現任何異常,說明有兩種情況:
1、請求沒有到達服務
2、請求到達服務,但是沒有進入到業務程式碼中就報錯了,導致沒有任何日誌被記錄
既然服務日誌沒有入手點,那就從nginx日誌中查詢一下吧,果然在nginx日誌中發現大量的請求失敗日誌,http狀態都是413。
通過分析nginx日誌,發現該請求已經由nginx轉發到server上,但是server返回了http 413,同時發現並非所有該介面的請求都失敗,只有request-length為5000多的失敗了,幾百的都是正常的,初步定位原因為server限制了請求大小的上限,而5000已經超過了這個值。
既然有了思路,就按照這個思路開始查詢問題,首先定位http訊息解碼器問題。
在Spring Webflux中,底層使用netty(SpringMVC底層使用的是tomcat)因此先定位該解碼器:reactor.netty.http.server.HttpRequestDecoderSpec
該解碼器實現如下:
package reactor.netty.http.server; import java.util.function.Function; import reactor.netty.http.HttpDecoderSpec; import reactor.netty.tcp.TcpServer; /** * A configuration builder to fine tune the {@link io.netty.handler.codec.http.HttpServerCodec} * (or more precisely the {@link io.netty.handler.codec.http.HttpServerCodec.HttpServerRequestDecoder}) for HTTP/1.1 * or {@link io.netty.handler.codec.http.HttpServerUpgradeHandler} for H2C. * <p> * Defaults are accessible as constants {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}, {@link #DEFAULT_MAX_HEADER_SIZE}, * {@link #DEFAULT_MAX_CHUNK_SIZE}, {@link #DEFAULT_INITIAL_BUFFER_SIZE}, {@link #DEFAULT_VALIDATE_HEADERS} and * {@link #DEFAULT_H2C_MAX_CONTENT_LENGTH}. * * @author Simon Baslé * @author Violeta Georgieva */ public final class HttpRequestDecoderSpec extends HttpDecoderSpec<HttpRequestDecoderSpec> { /** * The maximum length of the content of the H2C upgrade request. * By default the server will reject an upgrade request with non-empty content, * because the upgrade request is most likely a GET request. */ public static final int DEFAULT_H2C_MAX_CONTENT_LENGTH = 0; int h2cMaxContentLength = DEFAULT_H2C_MAX_CONTENT_LENGTH; @Override public HttpRequestDecoderSpec get() { return this; } /** * Configure the maximum length of the content of the H2C upgrade request. * By default the server will reject an upgrade request with non-empty content, * because the upgrade request is most likely a GET request. If the client sends * a non-GET upgrade request, {@code h2cMaxContentLength} specifies the maximum * length of the content of the upgrade request. * * @param h2cMaxContentLength the maximum length of the content of the upgrade request * @return this builder for further configuration */ public HttpRequestDecoderSpec h2cMaxContentLength(int h2cMaxContentLength) { this.h2cMaxContentLength = h2cMaxContentLength; return this; } /** * Build a {@link Function} that applies the http request decoder configuration to a * {@link TcpServer} by enriching its attributes. */ @SuppressWarnings("deprecation") Function<TcpServer, TcpServer> build() { HttpRequestDecoderSpec decoder = new HttpRequestDecoderSpec(); decoder.initialBufferSize = initialBufferSize; decoder.maxChunkSize = maxChunkSize; decoder.maxHeaderSize = maxHeaderSize; decoder.maxInitialLineLength = maxInitialLineLength; decoder.validateHeaders = validateHeaders; decoder.h2cMaxContentLength = h2cMaxContentLength; return tcp -> tcp.bootstrap(b -> HttpServerConfiguration.decoder(b, decoder)); } }
看了一下,沒有什麼有價值的線索,決定看下他的父類:reactor.netty.http.HttpDecoderSpec
該類實現程式碼如下:
package reactor.netty.http; import java.util.function.Supplier; /** * A configuration builder to fine tune the {@code HttpCodec} (or more precisely * the settings for the decoder) * * @author Violeta Georgieva */ public abstract class HttpDecoderSpec<T extends HttpDecoderSpec<T>> implements Supplier<T> { public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096; public static final int DEFAULT_MAX_HEADER_SIZE = 8192; public static final int DEFAULT_MAX_CHUNK_SIZE = 8192; public static final boolean DEFAULT_VALIDATE_HEADERS = true; public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; protected int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH; protected int maxHeaderSize = DEFAULT_MAX_HEADER_SIZE; protected int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE; protected boolean validateHeaders = DEFAULT_VALIDATE_HEADERS; protected int initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE; /** * Configure the maximum length that can be decoded for the HTTP request's initial * line. Defaults to {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}. * * @param value the value for the maximum initial line length (strictly positive) * @return this option builder for further configuration */ public T maxInitialLineLength(int value) { if (value <= 0) { throw new IllegalArgumentException( "maxInitialLineLength must be strictly positive"); } this.maxInitialLineLength = value; return get(); } public int maxInitialLineLength() { return maxInitialLineLength; } /** * Configure the maximum header size that can be decoded for the HTTP request. * Defaults to {@link #DEFAULT_MAX_HEADER_SIZE}. * * @param value the value for the maximum header size (strictly positive) * @return this option builder for further configuration */ public T maxHeaderSize(int value) { if (value <= 0) { throw new IllegalArgumentException("maxHeaderSize must be strictly positive"); } this.maxHeaderSize = value; return get(); } public int maxHeaderSize() { return maxHeaderSize; } /** * Configure the maximum chunk size that can be decoded for the HTTP request. * Defaults to {@link #DEFAULT_MAX_CHUNK_SIZE}. * * @param value the value for the maximum chunk size (strictly positive) * @return this option builder for further configuration */ public T maxChunkSize(int value) { if (value <= 0) { throw new IllegalArgumentException("maxChunkSize must be strictly positive"); } this.maxChunkSize = value; return get(); } public int maxChunkSize() { return maxChunkSize; } /** * Configure whether or not to validate headers when decoding requests. Defaults to * #DEFAULT_VALIDATE_HEADERS. * * @param validate true to validate headers, false otherwise * @return this option builder for further configuration */ public T validateHeaders(boolean validate) { this.validateHeaders = validate; return get(); } public boolean validateHeaders() { return validateHeaders; } /** * Configure the initial buffer size for HTTP request decoding. Defaults to * {@link #DEFAULT_INITIAL_BUFFER_SIZE}. * * @param value the initial buffer size to use (strictly positive) * @return this option builder for further configuration */ public T initialBufferSize(int value) { if (value <= 0) { throw new IllegalArgumentException("initialBufferSize must be strictly positive"); } this.initialBufferSize = value; return get(); } public int initialBufferSize() { return initialBufferSize; } }
看完程式碼,其中有一個方法引起了我的注意:
/**
* Configure the maximum length that can be decoded for the HTTP request's initial
* line. Defaults to {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}.
*
* @param value the value for the maximum initial line length (strictly positive)
* @return this option builder for further configuration
*/
public T maxInitialLineLength(int value) {
if (value <= 0) {
throw new IllegalArgumentException(
"maxInitialLineLength must be strictly positive");
}
this.maxInitialLineLength = value;
return get();
}
說明是maxInitialLineLength 這個值決定了他最多可以接受多大的http請求,接下來看一下他的定義:
protected int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH;
在檢視下DEFAULT_MAX_INITIAL_LINE_LENGTH 的定義:
public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096;
OK,到現在問題已經很明顯了,這個值就說明了,Spring Webflux搭建的web服務,預設會對大於4096位元組的請求返回 http 413錯誤。
知道了問題,下一步就是解決問題,解決問題過程太長就省略了,直接上解決方案,在專案中增加一個配置類:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NettyServerConfiguration implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
@Value("${netty.request.max-initial-line-length}")
private int maxInitialLineLength;
@Override
public void customize(NettyReactiveWebServerFactory factory) {
factory.addServerCustomizers(httpServer ->
httpServer.httpRequestDecoder(spec -> spec.maxInitialLineLength(maxInitialLineLength))
);
}
}
寫完程式碼後,上線,問題解決。