For the full repo look here
I have the following...
@Bean
fun messageAuthorizationManager(
messages: MessageMatcherDelegatingAuthorizationManager.Builder
): AuthorizationManager<Message<*>> {
messages
// Next 2 lines are required for requests without auth.
// Remove these if all paths require auth
.simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
.simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
.simpDestMatchers("/topic/greetings", "/app/hello").authenticated()
.simpDestMatchers("/topic/status", "/app/status").permitAll()
.anyMessage().authenticated()
return messages.build()
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
fun greeting(message: TestMessage): Greeting {
logger.info(message.toString())
return Greeting("Hello, " + message.name + "!")
}
@MessageMapping("/status")
@SendTo("/topic/status")
fun status(message: String): String {
return "Status: $message"
}
However, when I try to consume these in JS like
const subscribeToGreetings = useCallback(() => {
if (!client || !client.active) return;
setCanRetryGreetings(false);
console.log('Attempting to subscribe to greetings...');
client.subscribe('/topic/greetings',
(message) => {
console.log('Received greeting:', message);
setMessages(prev => [...prev, `Greeting: ${message.body}`]);
},
{
onError: (err) => {
console.error('Subscription error frame:', errmand, err.headers, err.body);
setMessages(prev => [...prev, `Permission denied: Cannot subscribe to greetings (${err.headers?.message || 'Unknown error'})`]);
setCanRetryGreetings(true);
}
}
);
}, [client]);
On the front end I just see
Attempting to subscribe to greetings...
I never actually see the onError part run. On the backend I do see....
2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@30c62bbb and result AuthorizationDecision [granted=false]
2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] o.s.w.s.m.StompSubProtocolHandler : Failed to send message to MessageChannel in session da876e1d-f266-eb9f-08e4-8dc7e068b0fa
...
Caused by: org.springframework.security.access.AccessDeniedException: Access Denied
at org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]
So what am I missing why isn't the front end getting notified that their request to subscribe has failed?
For the full repo look here
I have the following...
@Bean
fun messageAuthorizationManager(
messages: MessageMatcherDelegatingAuthorizationManager.Builder
): AuthorizationManager<Message<*>> {
messages
// Next 2 lines are required for requests without auth.
// Remove these if all paths require auth
.simpTypeMatchers(SimpMessageType.CONNECT).permitAll()
.simpTypeMatchers(SimpMessageType.DISCONNECT).permitAll()
.simpDestMatchers("/topic/greetings", "/app/hello").authenticated()
.simpDestMatchers("/topic/status", "/app/status").permitAll()
.anyMessage().authenticated()
return messages.build()
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
fun greeting(message: TestMessage): Greeting {
logger.info(message.toString())
return Greeting("Hello, " + message.name + "!")
}
@MessageMapping("/status")
@SendTo("/topic/status")
fun status(message: String): String {
return "Status: $message"
}
However, when I try to consume these in JS like
const subscribeToGreetings = useCallback(() => {
if (!client || !client.active) return;
setCanRetryGreetings(false);
console.log('Attempting to subscribe to greetings...');
client.subscribe('/topic/greetings',
(message) => {
console.log('Received greeting:', message);
setMessages(prev => [...prev, `Greeting: ${message.body}`]);
},
{
onError: (err) => {
console.error('Subscription error frame:', err.command, err.headers, err.body);
setMessages(prev => [...prev, `Permission denied: Cannot subscribe to greetings (${err.headers?.message || 'Unknown error'})`]);
setCanRetryGreetings(true);
}
}
);
}, [client]);
On the front end I just see
Attempting to subscribe to greetings...
I never actually see the onError part run. On the backend I do see....
2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] .s.m.a.i.AuthorizationChannelInterceptor : Failed to authorize message with authorization manager org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager@30c62bbb and result AuthorizationDecision [granted=false]
2025-02-07T12:59:38.360-05:00 DEBUG 50220 --- [websocket] [nio-7443-exec-4] o.s.w.s.m.StompSubProtocolHandler : Failed to send message to MessageChannel in session da876e1d-f266-eb9f-08e4-8dc7e068b0fa
...
Caused by: org.springframework.security.access.AccessDeniedException: Access Denied
at org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor.preSend(AuthorizationChannelInterceptor.java:75) ~[spring-security-messaging-6.4.2.jar:6.4.2]
So what am I missing why isn't the front end getting notified that their request to subscribe has failed?
Share Improve this question asked Feb 7 at 18:59 JackieJackie 23.5k41 gold badges169 silver badges327 bronze badges 3 |1 Answer
Reset to default 0The problem here is that your component code is not correct. Third parameter for subscribe
is StompHeaders
, not an error handler.
Error handler should be registered with your Client
, see onStompError
below.
Please see below code for a functioning React component. It is written with TypeScript, but it should be easy to remove types.
Tested with @stomp/stompjs 7.0.0.
import {useEffect, useState} from "react"
import {Client} from "@stomp/stompjs"
const StompTestComponent = () => {
const [messages, setMessages] = useState<string[]>([])
const [client, setClient] = useState<Client | null>(null)
useEffect(() => {
const stompClient = new Client({
brokerURL: "ws://localhost:8080/ws",
onConnect: () => console.log("STOMP client connected"),
onDisconnect: () => console.log("STOMP client disconnected"),
onStompError: frame => console.error("STOMP client error:", frame),
})
setClient(stompClient)
stompClient.activate()
// clean-up
return () => {
if (stompClient && stompClient.active) {
stompClient.deactivate().then(() => console.log("STOMP client deactivated"))
}
}
}, [])
const subscribeToGreetings = () => {
if (!(client && client.active)) return
console.log("Attempting to subscribe to greetings...")
client.subscribe(
"/topic/greetings",
message => {
console.log("Received greeting:", message)
setMessages(prev => [...prev, `Greeting: ${message.body}`])
}
)
}
if (!client) {
return <div>Initializing...</div>
}
return <div>
<button onClick={subscribeToGreetings}>Subscribe to Greetings</button>
<div>
{messages.map((message, idx) => <div key={idx}>{message}</div>)}
</div>
</div>
}
export default StompTestComponent
If you want custom error details, you can implement a custom StompSubProtocolErrorHandler
in your Spring Boot app like this
import org.springframework.security.access.AccessDeniedException
import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler
class CustomStompSubProtocolErrorHandler : StompSubProtocolErrorHandler() {
override fun handleClientMessageProcessingError(
clientMessage: Message<ByteArray>?,
ex: Throwable
): Message<ByteArray> {
val exception =
if (ex is MessageDeliveryException) ex.cause ?: ex
else ex
val errorCode = when (exception) {
is AccessDeniedException -> "ACCESS_DENIED"
else -> "UNKNOWN_ERROR"
}
val errorMessage = when (exception) {
is AccessDeniedException -> "You do not have permission to access this resource."
else -> exception.message ?: "An unexpected error occurred."
}
return createErrorMessage(errorMessage, errorCode)
}
companion object {
private fun createErrorMessage(
errorMessage: String,
errorCode: String
): Message<ByteArray> {
val headerAccessor = StompHeaderAccessor.create(StompCommand.ERROR).apply {
message = errorCode
}
return MessageBuilder.createMessage(
errorMessage.toByteArray(),
headerAccessor.messageHeaders
)
}
}
}
and register it like this
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry
.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
registry.setErrorHandler(CustomStompSubProtocolErrorHandler())
}
.simpTypeMatchers(SimpMessageType.SUBSCRIBE).permitAll()
make any change? – Roar S. Commented Feb 7 at 20:15