spring-projects / spring-projects/spring-framework

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

Open
#37,041 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
60.2k
Forks
38.8k
Avg merge
5d 2h
Merged PRs (30d)
27

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with StompSubProtocolHandler, which translates CONNECT_ACK into the CONNECTED frame and currently preserves only selected headers. Review the existing STOMP sub-protocol tests and add coverage for retaining the optional session header while version and heartbeat negotiation continue to work. Done means STOMP clients receive session, version, and heartbeat headers in CONNECTED.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
api, backend
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.