Skip to content
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

Can't unregister websocket when using decorators on ServerWebSocketContainer #8600

Closed
aperinot opened this issue Apr 19, 2023 · 0 comments · Fixed by #8601
Closed

Can't unregister websocket when using decorators on ServerWebSocketContainer #8600

aperinot opened this issue Apr 19, 2023 · 0 comments · Fixed by #8601

Comments

@aperinot
Copy link

aperinot commented Apr 19, 2023

Impacted artifact / version
spring-integration-websocket version 5.5.16

Even if not tested, the issue seems to be present in 5.5.17, and latest 6.X.X branch

Describe the bug
When adding decorator factories to the ServerWebSocketContainer, it is not possible to unregister the websocket endpoint.

When registering, the ServerWebSocketContainer code is as follow:

       @Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		WebSocketHandler webSocketHandler = this.webSocketHandler;

		if (this.decoratorFactories != null) {
			for (WebSocketHandlerDecoratorFactory factory : this.decoratorFactories) {
				webSocketHandler = factory.decorate(webSocketHandler);
			}
		}

		WebSocketHandlerRegistration registration = registry.addHandler(webSocketHandler, this.paths)
				.setHandshakeHandler(this.handshakeHandler)
				.addInterceptors(this.interceptors)
				.setAllowedOrigins(this.origins);

		configureSockJsOptionsIfAny(registration);
	}

As you can see, the addHandler is called with the latest returned webSocketHandler from the list of decoratorFactories.
If no decoratorFactories are set, the websocketHandler used is the default IntegrationWebSocketHandler.

However, the unregister code in IntegrationServletWebSocketHandlerRegistry is as follow:

      void removeRegistration(ServerWebSocketContainer serverWebSocketContainer) {
		List<String> patterns = this.dynamicRegistrations.remove(serverWebSocketContainer.getWebSocketHandler());
		if (this.dynamicHandlerMapping != null && !CollectionUtils.isEmpty(patterns)) {
			for (String pattern : patterns) {
				this.dynamicHandlerMapping.unregisterHandler(pattern);
			}
		}
	}

It tries to remove from the dynamicRegistrations by retrieving the handler with serverWebSocketContainer.getWebSocketHandler().
However the getWebSocketHandler() returns the default IntegrationWebSocketHandler, and not the decorated one, so the remove doesn't work, and it doesn't call the unregisterHandler() for the pattern.

To Reproduce

  @Autowired
  private IntegrationFlowContext integrationFlowContext;
 
  public void bugReproducer() {
    ServerWebSocketContainer serverWebSocketContainer = new ServerWebSocketContainer("/path")
        .setHandshakeHandler(new DefaultHandshakeHandler());

   // Adding a decorator triggers the bug
    serverWebSocketContainer.setDecoratorFactories(WebsocketHandlerDecorator::new);

   // The rest is some kind of boiler code, however i'm not sure how to make it shorter
    WebSocketOutboundMessageHandler webSocketOutboundMessageHandler = new WebSocketOutboundMessageHandler(
        serverWebSocketContainer);
    webSocketOutboundMessageHandler.afterPropertiesSet();

    StandardIntegrationFlow standardIntegrationFlow = IntegrationFlows.from(new DirectChannel())
        .split(new CustomSplitter(serverWebSocketContainer)).handle(webSocketOutboundMessageHandler).get();

    IntegrationFlowContext.IntegrationFlowRegistration dynamicServerFlow = integrationFlowContext
        .registration(standardIntegrationFlow).id("path").addBean(serverWebSocketContainer).register();
   
    // This call should kill the websocket endpoint previously registered
    dynamicServerFlow.destroy();
   }

  // Simple decorator for the default WebSocketHandler
private static class WebsocketHandlerDecorator implements WebSocketHandler
{
  private WebSocketHandler handler;

  private WebsocketHandlerDecorator(WebSocketHandler handler)
  {
    this.handler = handler;
  }

  @Override
  public void afterConnectionEstablished(WebSocketSession session) throws Exception
  {
    handler.afterConnectionEstablished(session);
  }

  @Override
  public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception
  {
    handler.handleMessage(session, message);
  }

  @Override
  public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception
  {
    handler.handleTransportError(session, exception);
  }

  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception
  {
    handler.afterConnectionClosed(session, closeStatus);
  }

  @Override
  public boolean supportsPartialMessages()
  {
    return handler.supportsPartialMessages();
  }
}

Expected behavior

The expected behavior shall be the unregistration of the endpoint.

Workaround

Here is a workaround to make it work as expected.

private static class CustomServerWebSocketContainer extends ServerWebSocketContainer
  {

    private WebSocketHandler lastHandler;

    public CustomServerWebSocketContainer(String... paths)
    {
      super(paths);
    }

    @Override
    public ServerWebSocketContainer setDecoratorFactories(WebSocketHandlerDecoratorFactory... factories)
    {

      WebSocketHandlerDecoratorFactory[] factoriesDecorator = Arrays.copyOf(factories, factories.length);

      if (factoriesDecorator.length > 0)
      {
      // We must decorate the decorator to save the last returned handler, so the getWebSocketHandler() returns that handler
        WebSocketHandlerDecoratorFactory lastFactory = factoriesDecorator[factoriesDecorator.length - 1];
        factoriesDecorator[factoriesDecorator.length - 1] = w ->
          {
            lastHandler = lastFactory.decorate(w);
            return lastHandler;
          };
      }

      return super.setDecoratorFactories(factoriesDecorator);
    }

    @Override
    public WebSocketHandler getWebSocketHandler()
    {
      if (lastHandler != null)
      {
        return lastHandler;
      }
      
      return super.getWebSocketHandler();
    }
  }
@aperinot aperinot added status: waiting-for-triage The issue need to be evaluated and its future decided type: bug labels Apr 19, 2023
@artembilan artembilan added this to the 6.1.0-RC1 milestone Apr 19, 2023
@artembilan artembilan added in: websocket and removed status: waiting-for-triage The issue need to be evaluated and its future decided labels Apr 19, 2023
artembilan added a commit to artembilan/spring-integration that referenced this issue Apr 19, 2023
Fixes spring-projects#8600

When we register a dynamic WebSocket endpoint and use a `WebSocketHandlerDecoratorFactory`
such an endpoint is not removed on an `IntegrationFlow` destruction.
The actual `WebSocketHandler` is decorated, however we still use an initial one
for condition.

* Refactor `IntegrationWebSocketContainer` to expose a `protected` setter for the
`WebSocketHandler` which is called from the `ServerWebSocketContainer` after decoration

**Cherry-pick to `6.0.x` & `5.5.x`**
garyrussell added a commit that referenced this issue Apr 19, 2023
* GH-8600: Fix WebSocket `removeRegistration`

Fixes #8600

When we register a dynamic WebSocket endpoint and use a `WebSocketHandlerDecoratorFactory`
such an endpoint is not removed on an `IntegrationFlow` destruction.
The actual `WebSocketHandler` is decorated, however we still use an initial one
for condition.

* Refactor `IntegrationWebSocketContainer` to expose a `protected` setter for the
`WebSocketHandler` which is called from the `ServerWebSocketContainer` after decoration

**Cherry-pick to `6.0.x` & `5.5.x`**

* Fix language in Javadocs

Co-authored-by: Gary Russell <grussell@vmware.com>

---------

Co-authored-by: Gary Russell <grussell@vmware.com>
garyrussell added a commit that referenced this issue Apr 19, 2023
* GH-8600: Fix WebSocket `removeRegistration`

Fixes #8600

When we register a dynamic WebSocket endpoint and use a `WebSocketHandlerDecoratorFactory`
such an endpoint is not removed on an `IntegrationFlow` destruction.
The actual `WebSocketHandler` is decorated, however we still use an initial one
for condition.

* Refactor `IntegrationWebSocketContainer` to expose a `protected` setter for the
`WebSocketHandler` which is called from the `ServerWebSocketContainer` after decoration

**Cherry-pick to `6.0.x` & `5.5.x`**

* Fix language in Javadocs

Co-authored-by: Gary Russell <grussell@vmware.com>

---------

Co-authored-by: Gary Russell <grussell@vmware.com>
garyrussell added a commit that referenced this issue Apr 19, 2023
* GH-8600: Fix WebSocket `removeRegistration`

Fixes #8600

When we register a dynamic WebSocket endpoint and use a `WebSocketHandlerDecoratorFactory`
such an endpoint is not removed on an `IntegrationFlow` destruction.
The actual `WebSocketHandler` is decorated, however we still use an initial one
for condition.

* Refactor `IntegrationWebSocketContainer` to expose a `protected` setter for the
`WebSocketHandler` which is called from the `ServerWebSocketContainer` after decoration

**Cherry-pick to `6.0.x` & `5.5.x`**

* Fix language in Javadocs

Co-authored-by: Gary Russell <grussell@vmware.com>

---------

Co-authored-by: Gary Russell <grussell@vmware.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants