spring boot 3.4.1, java 21
I've written a filter that does nothing more than check the request size against a configurable maximum and return 413 if the request is too large.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class RequestSizeFilter implements Filter {
@Value("${fam.max_request_size:10485760}")
private int maxContentBytes;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
try {
if (request.getContentLength() > maxContentBytes) {
throw new RuntimeException();
} else {
chain.doFilter(request, response);
}
} catch (Exception e) {
log.error("Detected overlarge request of %s bytes. Rejecting.".formatted(request.getContentLength()));
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
httpResponse.getWriter().println("Request exceeds maximum size of %s bytes".formatted(maxContentBytes));
}
}
}
When I run my automated tests against the endpoint this protects, the filter picks up the overlarge request correctly and rejects it with the appropriate status and error message. The problem seems to be that after that rejection, every other test that tries to hit that endpoint fails with
org.springframework.web.reactive.function.client.WebClientRequestException: Connection refused: localhost/[0:0:0:0:0:0:0:1]:8080
at app//org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:137)
Caused by:
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: localhost/[0:0:0:0:0:0:0:1]:8080
Caused by:
java.net.ConnectException: Connection refused
at java.base/sun.nio.ch.Net.pollConnect(Native Method)
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682)
at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:973)
at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:336)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:339)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:776)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
These are full end to end integration tests, so the context in which the app is running is the same as it would in production. The client we use to hit these endpoints is a custom extension of spring's built-in WebTestClient class with nothing more than a base url and a default authorization header being set for all requests.
The interesting thing is that if I disable the test that triggers this filter, all my other tests pass. Also, because test ordering is not guaranteed, I know that the same test that passes before the test that triggers this filter will fail if it runs after. The filter has to be leaving the server in some sort of unstable state, but I can't determine how or why. Is there some call I need in order to reset the service to a state where it's ready to accept incoming connections again?
What I ended up doing was converting from an implementation of Filter to an implementation of RequestBodyAdvice as outlined below:
@Component
@ControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class RequestSizeAdvice implements RequestBodyAdvice {
@Value("${fam.max_request_size:10485760}")
private int maxContentBytes;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
String bodyString = "";
try {
bodyString = new Gson().toJson(body);
} catch (OutOfMemoryError e) {
throw new MaxSizeExceededException("Unable to calculate body size. Maximum size is %s".formatted(maxContentBytes));
}
long bodySize = bodyString.getBytes().length;
if (bodySize > maxContentBytes) {
throw new MaxSizeExceededException("Body size %s greater than maximum %s".formatted(bodySize, maxContentBytes));
}
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}