Skip to content

accept Message subtype and supertype handlers in Java WebSocket APIs - #1382

Open
pjfanning wants to merge 2 commits into
apache:mainfrom
pjfanning:java-websocket-handler-wildcards
Open

pjfanning wants to merge 2 commits into
apache:mainfrom
pjfanning:java-websocket-handler-wildcards

Conversation

@pjfanning

@pjfanning pjfanning commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Motivation

Java sees Flow, Source, Sink and the stream shapes as invariant. So a WebSocket handler typed Flow<Message, TextMessage, NotUsed>, which is what Flow.of(Message.class).map(m -> TextMessage.create(...)) infers, cannot be passed to the Java WebSocket APIs. Neither can a Source<TextMessage, ?> as the outbound side. Our own docs and tests work around this with Arrays.<Message>asList(...) and (Message) TextMessage.create(...) casts. This is suggested PR 4 of #1378.

Modification

Widen the handler parameters, using wildcards on both sides (outputs ? extends Message, inputs ? super Message):

API Before After
WebSocketDirectives.handleWebSocketMessages* (6 overloads) Flow<Message, Message, T> Flow<? super Message, ? extends Message, T>
Http.singleWebSocketRequest (3 overloads) Flow<Message, Message, T> same as above
WebSocket.handleWebSocketRequestWith Flow<Message, Message, ?> same as above
testkit WSTestRequestBuilding.WS (2 overloads) Flow<Message, Message, T> same as above
WebSocketUpgrade.handleMessagesWith (9 overloads), plus the scaladsl WebSocketUpgrade overrides Graph<FlowShape<Message, Message>, ?>, Graph<SinkShape<Message>, ?>, Graph<SourceShape<Message>, ?> Graph<? extends FlowShape<? super Message, ? extends Message>, ?>, Graph<? extends SinkShape<? super Message>, ?>, Graph<? extends SourceShape<? extends Message>, ?>

The Graph parameters need the outer ? extends too. Without it, Java would not accept a Flow<Message, TextMessage, ...> as a Graph<FlowShape<? super Message, ? extends Message>, ...>.

Inside the Scala implementations, the wildcard arguments are cast to the previous exact types before being handed to scaladsl. This is safe because Graph, FlowShape, SinkShape and SourceShape are declared variant. Scala 2.13 accepts the widening as a plain type ascription, but Scala 3 does not infer that subtyping for the nested wildcards, hence the casts.

The Predicate<Message> (shouldCompress) parameters are unchanged, because Java lambdas already adapt to them.

MiMa reports IncompatibleSignatureProblem for the changed methods (12 in http-core, 3 in http; the other overloads are new in 2.0.0). The erased JVM signatures are unchanged, so existing compiled callers still link and existing Java sources still compile. The excludes are in new java-websocket-handler-wildcards.excludes files. http-testkit has MiMa disabled. A third-party class implementing the javadsl WebSocketUpgrade directly would need to update its overrides. That should be rare, since the interface exists for the request attribute.

The workarounds in the Java docs example and test app are removed: the half-closed-WebSocket-finite snippet in WebSocketClientExampleTest and WSEchoTestClientApp now use List.of(TextMessage...) sources and Flow<Message, TextMessage, ...> client flows directly, without Arrays.<Message>asList(...) witnesses or (Message) casts. The rendered docs pick up the simpler snippet.

Result

Java callers can pass WebSocket handlers whose outputs are Message subtypes and whose inputs are Message supertypes, on both the server and the client side.

Tests

  • sbt "http-tests/Test/testOnly org.apache.pekko.http.javadsl.server.directives.WebSocketHandlerTypesTest": passed on Scala 2.13 and 3.3.8. The new Java test uses a Flow<Message, TextMessage, NotUsed> handler throughout:
    • serverSideHandlersAcceptMessageSubtypes: every handleWebSocketMessages* overload (plain, shouldCompress, ForProtocol, ForOptionalProtocol), WebSocketUpgrade.handleMessagesWith(flow) and WebSocket.handleWebSocketRequestWith, each checked via WSProbe (send "hi", expect "HI", completion)
    • upgradeSinkAndSourceAcceptMessageSubtypeAndSupertype: handleMessagesWith(Sink.ignore(), Source<TextMessage, NotUsed>). Sink.ignore() is a Sink<Object, ...>, which covers the ? super Message input side
    • testkitClientHandlerAcceptsMessageSubtype: testkit WS(...) with a Flow<Message, TextMessage, NotUsed> client handler
    • singleWebSocketRequestAcceptsMessageSubtype: Http.singleWebSocketRequest with a Flow<Message, TextMessage, CompletionStage<Message>> against a real bound server
    • all of these fail to compile against the old signatures
  • Existing specs still pass: sbt "http-tests/Test/testOnly org.apache.pekko.http.scaladsl.server.directives.WebSocketDirectivesSpec" "http-core/Test/testOnly org.apache.pekko.http.impl.engine.ws.WebSocketServerSpec org.apache.pekko.http.impl.engine.ws.WebSocketIntegrationSpec"
  • sbt http-core/mimaReportBinaryIssues http/mimaReportBinaryIssues: passed with the new excludes
  • sbt docs/Test/compile: passed before the docs cleanup commit; that commit was only formatted locally (javafmt, no changes) and its compile is left to CI
  • scalafmt checks for http-core / http / http-testkit, http-tests/Test/javafmtCheck (JDK 17) and header checks: passed

References

Refs #1378

Motivation:
Java sees Flow, Source, Sink and the stream shapes as invariant, so a
handler typed Flow<Message, TextMessage, NotUsed> (what
Flow.of(Message.class).map(m -> TextMessage.create(...)) infers) or a
Source<TextMessage, ?> could not be passed to the Java WebSocket APIs.

Modification:
Widen the handler parameters to Flow<? super Message, ? extends Message,
T>, and to Graph<? extends FlowShape<? super Message, ? extends
Message>, ?> / Graph<? extends SinkShape<? super Message>, ?> /
Graph<? extends SourceShape<? extends Message>, ?>, in
WebSocketDirectives.handleWebSocketMessages*, Http.singleWebSocketRequest,
WebSocket.handleWebSocketRequestWith, WebSocketUpgrade.handleMessagesWith
(and the scaladsl overrides) and testkit WSTestRequestBuilding.WS. Add
MiMa excludes for the generic-signature-only changes.

Result:
Java callers can pass WebSocket handlers with Message subtype outputs and
supertype inputs. The erased signatures are unchanged.

Tests:
- sbt "http-tests/Test/testOnly ...WebSocketHandlerTypesTest" - passed on Scala 2.13 and 3.3.8
- sbt "http-tests/Test/testOnly ...WebSocketDirectivesSpec" "http-core/Test/testOnly ...WebSocketServerSpec ...WebSocketIntegrationSpec" - passed
- sbt http-core/mimaReportBinaryIssues http/mimaReportBinaryIssues - passed

References:
Refs apache#1378
@pjfanning pjfanning added this to the 2.0.0-M3 milestone Oct 27, 2026
Motivation:
With the Java WebSocket APIs accepting Message subtypes, the
Arrays.<Message>asList(...) witnesses and (Message) casts in the docs
example and WSEchoTestClientApp are no longer needed.

Modification:
Use List.of(TextMessage...) sources and Flow<Message, TextMessage, ...>
client flows directly in WebSocketClientExampleTest (the
half-closed-WebSocket-finite snippet) and WSEchoTestClientApp.

Result:
The docs show the natural way to write a Java WebSocket client flow.

Tests:
- sbt docs/Test/javafmt http-core/Test/javafmt - no changes needed
- compile not run locally (machine overloaded); left to CI

References:
Refs apache#1378
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant