Skip to content

Pass preflight request to upstream. Fixes gh-2472 #2582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-gateway.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2396,6 +2396,31 @@ In the preceding example, CORS requests are allowed from requests that originate
To provide the same CORS configuration to requests that are not handled by some gateway route predicate, set the `spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping` property to `true`.
This is useful when you try to support CORS preflight requests and your route predicate does not evalute to `true` because the HTTP method is `options`.

== Handle CORS Preflight Request By Upstream

You can configure the gateway to pass CORS preflight request to upstream per route configuration.
The following example shows how to do this:

.application.yml
====
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: handle_pre_flight_request_by_upstream
uri: ${upstream-uri}
handle-preflight-request-by-upstream: true
predicates:
- Path=/some-path/**
----
====

In the preceding example, CORS preflight request will be handled by upstream for this route configuration, no matter what `globalcors` is configured.

Route configuration without `handle-preflight-request-by-upstream` or explicitly setting to `false` will obey gateway CORS configuration.

== Actuator API

The `/gateway` actuator endpoint lets you monitor and interact with a Spring Cloud Gateway application.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ public int getOrder() {
return 0;
}

@Override
public boolean handlePreFlightRequest() {
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface GatewayFilter extends ShortcutConfigurable {
public interface GatewayFilter extends PreFlightRequestFilter, ShortcutConfigurable {

/**
* Name key.
Expand All @@ -52,4 +52,9 @@ public interface GatewayFilter extends ShortcutConfigurable {
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

@Override
default boolean handlePreFlightRequest() {
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface GlobalFilter {
public interface GlobalFilter extends PreFlightRequestFilter {

/**
* Process the Web request and (optionally) delegate to the next {@code WebFilter}
Expand All @@ -39,4 +39,9 @@ public interface GlobalFilter {
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

@Override
default boolean handlePreFlightRequest() {
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
* @author Spencer Gibb
* @author Biju Kunjummen
*/
public class NettyRoutingFilter implements GlobalFilter, Ordered {
public class NettyRoutingFilter implements GlobalFilter, PreFlightRequestFilter, Ordered {

/**
* The order of the NettyRoutingFilter. See {@link Ordered#LOWEST_PRECEDENCE}.
Expand Down Expand Up @@ -198,6 +198,11 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return responseFlux.then(chain.filter(exchange));
}

@Override
public boolean handlePreFlightRequest() {
return true;
}

protected ByteBuf getByteBuf(DataBuffer dataBuffer) {
if (dataBuffer instanceof NettyDataBuffer) {
NettyDataBuffer buffer = (NettyDataBuffer) dataBuffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return this.delegate.filter(exchange, chain);
}

@Override
public boolean handlePreFlightRequest() {
return delegate.handlePreFlightRequest();
}

@Override
public int getOrder() {
return this.order;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2013-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.gateway.filter;

/**
* Indicate a filter should handle pre-flight request and pass the request to upstream.
* Only filters that affect route process should return true with handlePreFlightRequest method.
*
* @author Tommas Yuan
*/
public interface PreFlightRequestFilter {

/**
* @return {@code true} to indicate this filter will handle pre-flight request
*/
boolean handlePreFlightRequest();

}
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,9 @@ private String getHint(String serviceId) {
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
}

@Override
public boolean handlePreFlightRequest() {
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,9 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}

@Override
public boolean handlePreFlightRequest() {
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ public class FilteringWebHandler implements WebHandler {

private final List<GatewayFilter> globalFilters;

private final List<GatewayFilter> preflightRequestFilters;

public FilteringWebHandler(List<GlobalFilter> globalFilters) {
this.globalFilters = loadFilters(globalFilters);
this.preflightRequestFilters = this.globalFilters.stream().filter(filter -> filter.handlePreFlightRequest())
.toList();
}

private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
Expand Down Expand Up @@ -88,6 +92,23 @@ public Mono<Void> handle(ServerWebExchange exchange) {
return new DefaultGatewayFilterChain(combined).filter(exchange);
}

public Mono<Void> handlePreFlight(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
List<GatewayFilter> preflightRequestFilters = route.getFilters().stream()
.filter(filter -> filter.handlePreFlightRequest()).toList();

List<GatewayFilter> combined = new ArrayList<>(this.preflightRequestFilters);
combined.addAll(preflightRequestFilters);

AnnotationAwareOrderComparator.sort(combined);

if (logger.isDebugEnabled()) {
logger.debug("Sorted preflight request filters: " + combined);
}

return new DefaultGatewayFilterChain(combined).filter(exchange);
}

private static class DefaultGatewayFilterChain implements GatewayFilterChain {

private final int index;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsUtils;
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
import org.springframework.web.server.ServerWebExchange;

Expand Down Expand Up @@ -73,6 +76,30 @@ private static Integer getPortProperty(Environment environment, String prefix) {
return environment.getProperty(prefix + "port", Integer.class);
}

@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
Mono<Object> mono = super.getHandler(exchange);

if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return mono.flatMap(handler -> {
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
// pass pre-flight request to upstream, if the request is permitted
ServerHttpResponse response = exchange.getResponse();
if (route != null && route.handlePreflightRequest()) {
// remove cors response headers generated by gateway
HttpHeaders headers = response.getHeaders();
headers.remove(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);
headers.remove(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS);
headers.remove(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS);
return webHandler.handlePreFlight(exchange).then(Mono.just(handler));
}
return Mono.just(handler);
});
}

return mono;
}

@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// don't handle requests on management port if set and different than server port
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ public class Route implements Ordered {

private final Map<String, Object> metadata;

private boolean handlePreflightRequest;

private Route(String id, URI uri, int order, AsyncPredicate<ServerWebExchange> predicate,
List<GatewayFilter> gatewayFilters, Map<String, Object> metadata) {
List<GatewayFilter> gatewayFilters, Map<String, Object> metadata, boolean handlePreflightRequest) {
this.id = id;
this.uri = uri;
this.order = order;
this.predicate = predicate;
this.gatewayFilters = gatewayFilters;
this.metadata = metadata;
this.handlePreflightRequest = handlePreflightRequest;
}

public static Builder builder() {
Expand All @@ -74,7 +77,8 @@ public static Builder builder(RouteDefinition routeDefinition) {
return new Builder().id(routeDefinition.getId())
.uri(routeDefinition.getUri())
.order(routeDefinition.getOrder())
.metadata(routeDefinition.getMetadata());
.metadata(routeDefinition.getMetadata())
.handlePreflightRequestByUpstream(routeDefinition.isHandlePreflightRequestByUpstream());
// @formatter:on
}

Expand All @@ -87,7 +91,8 @@ public static AsyncBuilder async(RouteDefinition routeDefinition) {
return new AsyncBuilder().id(routeDefinition.getId())
.uri(routeDefinition.getUri())
.order(routeDefinition.getOrder())
.metadata(routeDefinition.getMetadata());
.metadata(routeDefinition.getMetadata())
.handlePreflightRequestByUpstream(routeDefinition.isHandlePreflightRequestByUpstream());
// @formatter:on
}

Expand Down Expand Up @@ -115,6 +120,10 @@ public Map<String, Object> getMetadata() {
return Collections.unmodifiableMap(metadata);
}

public boolean handlePreflightRequest() {
return handlePreflightRequest;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -127,12 +136,14 @@ public boolean equals(Object o) {
return this.order == route.order && Objects.equals(this.id, route.id) && Objects.equals(this.uri, route.uri)
&& Objects.equals(this.predicate, route.predicate)
&& Objects.equals(this.gatewayFilters, route.gatewayFilters)
&& Objects.equals(this.metadata, route.metadata);
&& Objects.equals(this.metadata, route.metadata)
&& this.handlePreflightRequest == route.handlePreflightRequest;
}

@Override
public int hashCode() {
return Objects.hash(this.id, this.uri, this.order, this.predicate, this.gatewayFilters, this.metadata);
return Objects.hash(this.id, this.uri, this.order, this.predicate, this.gatewayFilters, this.metadata,
this.handlePreflightRequest);
}

@Override
Expand All @@ -144,6 +155,7 @@ public String toString() {
sb.append(", predicate=").append(predicate);
sb.append(", gatewayFilters=").append(gatewayFilters);
sb.append(", metadata=").append(metadata);
sb.append(", handlePreflightRequest=").append(handlePreflightRequest);
sb.append('}');
return sb.toString();
}
Expand All @@ -160,6 +172,8 @@ public abstract static class AbstractBuilder<B extends AbstractBuilder<B>> imple

protected Map<String, Object> metadata = new HashMap<>();

protected boolean handlePreflightRequestByUpstream;

protected AbstractBuilder() {
}

Expand Down Expand Up @@ -210,6 +224,11 @@ public B metadata(String key, Object value) {
return getThis();
}

public B handlePreflightRequestByUpstream(boolean handlePreflightRequestByUpstream) {
this.handlePreflightRequestByUpstream = handlePreflightRequestByUpstream;
return getThis();
}

public abstract AsyncPredicate<ServerWebExchange> getPredicate();

public B replaceFilters(List<GatewayFilter> gatewayFilters) {
Expand Down Expand Up @@ -237,7 +256,8 @@ public Route build() {
AsyncPredicate<ServerWebExchange> predicate = getPredicate();
Assert.notNull(predicate, "predicate can not be null");

return new Route(this.id, this.uri, this.order, predicate, this.gatewayFilters, this.metadata);
return new Route(this.id, this.uri, this.order, predicate, this.gatewayFilters, this.metadata,
this.handlePreflightRequestByUpstream);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public class RouteDefinition {

private int order = 0;

private boolean handlePreflightRequestByUpstream;

public RouteDefinition() {
}

Expand Down Expand Up @@ -125,6 +127,14 @@ public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}

public boolean isHandlePreflightRequestByUpstream() {
return handlePreflightRequestByUpstream;
}

public void setHandlePreflightRequestByUpstream(boolean handlePreflightRequestByUpstream) {
this.handlePreflightRequestByUpstream = handlePreflightRequestByUpstream;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -136,18 +146,21 @@ public boolean equals(Object o) {
RouteDefinition that = (RouteDefinition) o;
return this.order == that.order && Objects.equals(this.id, that.id)
&& Objects.equals(this.predicates, that.predicates) && Objects.equals(this.filters, that.filters)
&& Objects.equals(this.uri, that.uri) && Objects.equals(this.metadata, that.metadata);
&& Objects.equals(this.uri, that.uri) && Objects.equals(this.metadata, that.metadata)
&& this.handlePreflightRequestByUpstream == that.handlePreflightRequestByUpstream;
}

@Override
public int hashCode() {
return Objects.hash(this.id, this.predicates, this.filters, this.uri, this.metadata, this.order);
return Objects.hash(this.id, this.predicates, this.filters, this.uri, this.metadata, this.order,
this.handlePreflightRequestByUpstream);
}

@Override
public String toString() {
return "RouteDefinition{" + "id='" + id + '\'' + ", predicates=" + predicates + ", filters=" + filters
+ ", uri=" + uri + ", order=" + order + ", metadata=" + metadata + '}';
+ ", uri=" + uri + ", order=" + order + ", metadata=" + metadata + "handlePreFlightRequest="
+ handlePreflightRequestByUpstream + '}';
}

}
Loading