micronaut-core
micronaut-core copied to clipboard
CorsFilter ignores http methods available via filters
Expected Behavior
The CORS filter should allow pre-flight requests for any HTTP method that is handled by a filter. Alternatively the filter should be able to specify which methods are available for the handled paths.
Actual Behaviour
The CORS filter only considers paths handled by routes, not paths handled by filters. It gets to the point in CorsFilter.handleRequest where it is considering which HTTP methods are available, and it finds none. The CORS pre-flight is not processed by CorsFilter, resulting in a 401 Forbidden.
An example use case for such a filter is one which uses a ProxyHttpClient to forward all requests under a specific path to another server.
This was working in Micronaut 3.3. Seems to have been broken by https://github.com/micronaut-projects/micronaut-core/pull/7367.
Steps To Reproduce
- Create filter which matches a path that is not handled by any controller.
- Issue a valid CORS pre-flight request against a path matching the filter.
- The response will be 401 when it should be 200.
For example, a filter like this:
@Filter("/proxy/**")
public class ProxyFilter implements HttpServerFilter {
private final ProxyHttpClient client;
@Property(name = "micronaut.http.services.upstream.url")
private String upstreamUrl;
public ProxyFilter(ProxyHttpClient client) {
this.client = client;
}
@Override
public Publisher<MutableHttpResponse<?>> doFilter(
HttpRequest<?> request, ServerFilterChain chain) {
if (!isPreflightRequest(request)) {
return Publishers.map(client.proxy(mutateRequest(request)), response -> response);
} else {
return chain.proceed(request);
}
}
@VisibleForTesting
@SneakyThrows
MutableHttpRequest<?> mutateRequest(HttpRequest<?> request) {
URI uri = new URI(upstreamUrl);
MutableHttpRequest<?> mutableHttpRequest =
request.mutate().uri(b ->
b.scheme(uri.getScheme()).host(uri.getHost()).port(uri.getPort()));
mutableHttpRequest.getHeaders().set(HttpHeaders.HOST, uri.getHost());
return mutableHttpRequest;
}
boolean isPreflightRequest(HttpRequest<?> request) {
io.micronaut.http.HttpHeaders headers = request.getHeaders();
Optional<String> origin = headers.getOrigin();
return origin.isPresent()
&& headers.contains(ACCESS_CONTROL_REQUEST_METHOD)
&& HttpMethod.OPTIONS == request.getMethod();
}
}
Tests something like this should detect the problem:
@MicronautTest(transactional = false)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ProxyFilterTest {
@Inject
private EmbeddedServer server;
@Inject
@Client("/")
private HttpClient apiClient;
@Test
void testProxyOptionsRequest_success() {
String validEstimateUri = "/proxy/test";
HttpResponse<String> rsp =
apiClient
.toBlocking()
.exchange(
HttpRequest.OPTIONS(validEstimateUri)
.accept(MediaType.ALL)
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(HttpHeaders.ORIGIN, "http://localhost"),
String.class);
assertThat(rsp.code(), is(200));
assertThat(rsp.header(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS), is("POST"));
assertThat(rsp.header(HttpHeaders.ACCESS_CONTROL_MAX_AGE), is("172800"));
assertThat(rsp.header(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN), is("http://localhost"));
assertThat(rsp.header(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS), is("true"));
}
@Test
void testProxyOptionsRequest_badOrigin() {
String validEstimateUri = "/proxy/test";
final HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, () ->
apiClient
.toBlocking()
.exchange(
HttpRequest.OPTIONS(validEstimateUri)
.accept(MediaType.ALL)
.header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(HttpHeaders.ORIGIN, "http://invalid.origin"),
String.class));
assertThat(thrown.getStatus(), is(HttpStatus.UNAUTHORIZED));
}
}
A workaround is to make sure the custom filter runs before CorsFilter, and set AVAILABLE_HTTP_METHODS during pre-flight requests.
@Override
public Publisher<MutableHttpResponse<?>> doFilter(
HttpRequest<?> request, ServerFilterChain chain) {
if (!isPreflightRequest(request)) {
return Publishers.map(client.proxy(mutateRequest(request)), response -> response);
} else {
return chain.proceed(request.mutate()
// this is the workaround
.setAttribute(AVAILABLE_HTTP_METHODS, Arrays.asList(HttpMethod.values())));
}
}
Environment Information
No response
Example Application
No response
Version
3.5.4 (likely started in 3.4.x)
This is still an issue with Micronaut 4.7.x. We did find a workaround, which was to create a fake controller with a pattern that matches the filter. For example:
@ServerFilter("/proxy/path1/**")
public class MyProxyFilter {
@Controller("/proxy/path1/{+ignored}")
public static class FakeMyProxyController {
@Post
@Get
void doNothing(@PathVariable String ignored) {}
}
// ...rest of filter...
Since the filter handles all the requests, the controller method never gets hit. The controller just exists so a route is found by CorsFilter when it's validating pre-flight requests.
Can't you just place the filter before the cors filter?
Can you please confirm that this is still an issue in the latest version of Micronaut?