Spring Cloud Gateway gateway forwarding websocket service configuration

Spring Cloud Gateway is the unified entrance for all microservices.

1. Spring Cloud Gateway key terms

  • Route: Routing, the basic component module of gateway configuration. A Route module is defined by an ID, a target URI, a set of assertions, and a set of filters. If the assertion is true, the route matches and the target URI is accessed.
  • Predicate: Assertion, which can be used to match anything from an HTTP request.
  • Filter: Filter, which can be used to intercept and modify requests, and perform secondary processing on upstream responses. Filter An instance of the org.springframework.cloud.gateway.filter.GatewayFilter class.

2. Spring Cloud Gateway processing flow

The client makes a request to Spring Cloud Gateway. Then find the route matching the request in the Gateway Handler Mapping and send it to the Gateway Web Handler. The Handler then sends the request to our actual service to execute the business logic through the specified filter chain, and then returns. Filters are separated by dotted lines because filters may execute business logic before (“pre”) or after (“post”) sending the proxy request.

3. In the Spring Cloud Gateway, forward the websocket request through the following configuration in the yml configuration file:

spring:
  cloud:
    gateway:
      routes:
        - id: websocket1
          uri: lb:ws://serviceName #Usage method 2: websocket configuration, calling serviceName through nacos registration center
          predicates:
            - Path=/websocket

When the websocket service is socketio based on netty, netty needs to open a separate port for access. The above method requires directly specifying the port of the websocket service. When there are multiple websocket services, you can configure multiple identical routing rules, each specifying a socketio service, and then pass Weights achieve load balancing:

spring:
  cloud:
    gateway:
      routes:
        - id: websocket1
          uri: ws://127.0.0.1:8081
          predicates:
            - Path=/socket
            - Weight=group1,50
        - id: websocket2
          uri: ws://127.0.0.1:8082
          predicates:
            - Path=/socket
            - Weight=group1,50

4. When forwarding websocket requests in Spring Cloud Gateway, the problem of immediate disconnection occurs after connecting to the websocket, and an error java.lang.UnsupportedOperationException is reported. The details are as follows:

2023-10-24 10:05:23.433 ERROR 12636 --- [ctor-http-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter: [6726d297-6] Error [java.lang.UnsupportedOperationException] for HTTP GET " /socket/?EIO=3 & transport=websocket", but ServerHttpResponse already committed (200 OK)
2023-10-24 10:05:23.433 ERROR 12636 --- [ctor-http-nio-6] r.n.http.server.HttpServerOperations: [6726d297-1, L:/192.168.20.5:9099 - R:/192.168. 20.5:9099] Error finishing response. Closing connection

java.lang.UnsupportedOperationException: null
at org.springframework.http.ReadOnlyHttpHeaders.put(ReadOnlyHttpHeaders.java:126) ~[spring-web-5.3.20.jar:5.3.20]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? org.springframework.web.cors.reactive.CorsWebFilter [DefaultWebFilterChain]
*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
\t
*__checkpoint ? HTTP GET "/socket/?EIO=3 & transport=websocket" [ExceptionHandlingWebHandler]
Original Stack Trace:
at org.springframework.http.ReadOnlyHttpHeaders.put(ReadOnlyHttpHeaders.java:126) ~[spring-web-5.3.20.jar:5.3.20]

Through analysis, it was found that Gateway uses the following method when handling cross-domain:

 // Cross-domain configuration source
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        //Set cross-domain configuration information
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // Allow all request sources to make cross-domain requests
        //corsConfiguration.addAllowedOrigin("*");
        ...

It needs to be changed to reactor responsive mode, as follows:

return (ServerWebExchange ctx, WebFilterChain chain) -> {<!-- -->
            ServerHttpRequest request = ctx.getRequest();
            // Use the cross-domain detection tool class that comes with SpringMvc to determine whether the current request is cross-domain.
            if (!CorsUtils.isCorsRequest(request)) {<!-- -->
                return chain.filter(ctx);
            }
            HttpHeaders requestHeaders = request.getHeaders(); // Get request headers
            ServerHttpResponse response = ctx.getResponse(); // Get the response object
            HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod(); // Get the request method object
            HttpHeaders headers = response.getHeaders(); // Get response headers
            headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); // Add the request source (protocol + ip + port) in the request header to the response header (equivalent to allowedOrigins in yml)
            headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
            if (requestMethod != null) {<!-- -->
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name()); // Allowed response methods (GET/POST, etc., equivalent to allowedMethods in yml)
            }
            headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); // Allow cookies to be carried in requests (equivalent to allowCredentials in yml)
            headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); // Header information allowed to be carried in the request (equivalent to allowedHeaders in yml)
            headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "18000L"); // The validity period of this cross-domain detection (in milliseconds, equivalent to maxAge in yml)
            if (request.getMethod() == HttpMethod.OPTIONS) {<!-- --> // Directly request the option and return the result
                response.setStatusCode(HttpStatus.OK);
                return Mono.empty();
            }
            return chain.filter(ctx); // If it is not an option request, it will be released.
        };

5. Spring Cloud Gateway handles cross-domain processing, which can be implemented through yml configuration, such as:

 gateway:
      # Global cross-domain configuration
      globalcors:
        # Solve the problem of options request being intercepted
        add-to-simple-url-handler-mapping: true
        cors-configurations:
          #Intercepted requests
          '[/**]':
            # Allow cross-domain requests
            #allowedOrigins: "*" # Configuration before spring boot2.4
            allowedOriginPatterns: "*" # Configuration after spring boot2.4
            # Allow header information carried in the request
            allowedHeaders: "*"
            # Run cross-domain requests
            allowedMethods: "*"
            # Whether to allow cookies to be carried
            allowCredentials: true
            # Validity period of cross-domain detection, unit s
            maxAge: 3600

Cross-domain configuration classes can also be defined through coding, such as:

@Configuration
public class CorsConfig {<!-- -->

    @Bean
    public WebFilter corsFilter() {<!-- -->
        return (ServerWebExchange ctx, WebFilterChain chain) -> {<!-- -->
            ServerHttpRequest request = ctx.getRequest();
            // Use the cross-domain detection tool class that comes with SpringMvc to determine whether the current request is cross-domain.
            if (!CorsUtils.isCorsRequest(request)) {<!-- -->
                return chain.filter(ctx);
            }
            HttpHeaders requestHeaders = request.getHeaders(); // Get request headers
            ServerHttpResponse response = ctx.getResponse(); // Get the response object
            HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod(); // Get the request method object
            HttpHeaders headers = response.getHeaders(); // Get response headers
            headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); // Add the request source (protocol + ip + port) in the request header to the response header (equivalent to allowedOrigins in yml)
            headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
            if (requestMethod != null) {<!-- -->
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name()); // Allowed response methods (GET/POST, etc., equivalent to allowedMethods in yml)
            }
            headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); // Allow cookies to be carried in requests (equivalent to allowCredentials in yml)
            headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*"); // Header information allowed to be carried in the request (equivalent to allowedHeaders in yml)
            headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "18000L"); // The validity period of this cross-domain detection (in milliseconds, equivalent to maxAge in yml)
            if (request.getMethod() == HttpMethod.OPTIONS) {<!-- --> // Directly request the option and return the result
                response.setStatusCode(HttpStatus.OK);
                return Mono.empty();
            }
            return chain.filter(ctx); // If it is not an option request, it will be released.
        };
    }

}