1. 程式人生 > 其它 >Spring Webflux出現http 413問題的解決過程

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))
        );
    }
}

寫完程式碼後,上線,問題解決。