Skip to content

No easy way to add session header in the STOMP CONNECTED frame #37041

Description

@navjot-singh02

We had a requirement where we wanted to add the session header to the CONNECTED stomp frame to share it with the Stomp clients. As per STOMP's documentation, it is possible to add an optional session header to the CONNECTED frame to uniquely identify the session: https://stomp.github.io/stomp-specification-1.2.html#CONNECTED_Frame

In order to achieve this I added an outbound channel interceptor to add the session header to CONNECT_ACK messages. Here is the implementation that I ended up with:

internal object StompConnectedFrameInterceptor : ChannelInterceptor {
    /**
     * Unfortunately Spring Api doesn't expose the supported Stomp versions, so we have to replicate here
     */
    internal val SUPPORTED_STOMP_VERSIONS = listOf("1.2", "1.1", "1.0")

    override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
        val messageAccessor = StompHeaderAccessor.wrap(message)
        if (messageAccessor.messageType != SimpMessageType.CONNECT_ACK) {
            return message
        }

        val sessionId = messageAccessor.sessionId ?: return message

        /**
         * Create the CONNECTED frame ourselves so Spring does not drop the custom
         * session header during its CONNECT_ACK -> CONNECTED translation. Since this
         * bypasses Spring's translation, we must also set version and heartbeat here.
         * See StompSubProtocolHandler for details
         */
        val connectedAccessor = StompHeaderAccessor.create(StompCommand.CONNECTED)
        connectedAccessor.setNativeHeader(StompHeaders.SESSION, sessionId)

        connectedAccessor.version = negotiateStompVersion(messageAccessor)

        val heartbeat = getHeartbeat(messageAccessor)
        connectedAccessor.setHeartbeat(heartbeat[0], heartbeat[1])

        return MessageBuilder.fromMessage(message)
            .copyHeaders(connectedAccessor.messageHeaders)
            .build()
    }

    private fun negotiateStompVersion(connectAckAccessor: StompHeaderAccessor): String {
        val connectMessage = connectAckAccessor.getHeader(StompHeaderAccessor.CONNECT_MESSAGE_HEADER) as? Message<*>
        val connectAccessor = connectMessage?.let { StompHeaderAccessor.wrap(it) }
        val acceptedVersion = connectAccessor?.acceptVersion?.let { acceptVersions ->
            SUPPORTED_STOMP_VERSIONS.firstOrNull { it in acceptVersions }
        }

        return acceptedVersion ?: throw IllegalArgumentException(
            "No supported STOMP version found in CONNECT message. Supported versions: $SUPPORTED_STOMP_VERSIONS",
        )
    }

    private fun getHeartbeat(connectAckAccessor: StompHeaderAccessor): LongArray {
        val heartbeat = connectAckAccessor.getHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER) as? LongArray
        return heartbeat?.takeIf { it.size == 2 } ?: longArrayOf(0, 0)
    }
}

I had to do the version negotiation and heartbeat negotiation myself. If I skip it, then there were no version and heartbeat headers in the frame received by clients. The downside of this application is that much of the Spring's internal code is being rewritten here. Also the STOMP versions are also being redeclared here.

I looked into the code and found out that the StompSubProtocolHandler is responsible for translating the CONNECT_ACK to the actual CONNECTED frame. During this translation all the headers except the version and heartbeat are ignored, so essentially even if we set the session header it will be ignored by the translation logic.

Is there a purpose to not allow optional headers defined by the STOMP protocol specifically the session header? If there is no specific purpose, could it be supported? I would be happy to contribute if required

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions