Search code examples
javaspringspring-bootservlet-filters

After filter fails, all further requests to that endpoint are returning "connection refused"


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?


Solution

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