Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

HTTP/2 client ALPN fallback needs a handshake-complete signal that the TLS stage does not emit

オープン
#1,249 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
35/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
java, scala

調査の方向性

Http2AlpnSupport.scala:74 と Http2.scala:241,275 から始め、次に ProtocolSwitch.scala と Http2Blueprint.scala:146,327 を追跡します。issue に記載されている materialization ごとの engine シグナルとクライアント側の 4 ポート switch を、初期の inbound 要素のバッファリングも含めて実装します。提供されたプロトコルを提示し、ネゴシエートされた HTTP/1.1 または HTTP/2 のスタックを deadlock なしで選択できれば完了です。

索引モデルが issue の本文から書いたものです。

説明

Split out from #483 (client-side HTTP/1.1 vs HTTP/2 protocol negotiation), which needs this solved first.

Code anchors below are against main @ 85d7243e7.

The blocker

The server already negotiates via ALPN and switches stacks: Http2Ext.httpsWithAlpn (Http2.scala:241) parks the chosen protocol in a mutable cell from setHandshakeApplicationProtocolSelector, and ProtocolSwitch (ProtocolSwitch.scala) installs either the HTTP/1.1 or HTTP/2 stack.

ProtocolSwitch makes that decision on the first inbound SessionBytes. That is sound server-side, because in HTTP the client always speaks first — there is always an inbound byte to trigger on.

Mirroring it on the client deadlocks. If negotiation lands on http/1.1, the server sends nothing until it receives a request, and the switch is sitting on that request waiting to learn the protocol. Neither side moves.

So the client needs a different trigger: a "handshake complete, ALPN result known" signal, delivered without depending on inbound application bytes.

Why the obvious signals don't work

  • Pekko's TLS stage emits no handshake-complete event. SslTlsInbound is only ever SessionBytes or SessionTruncated, and SessionBytes only arrives when there is application data.
  • SSLSession (carried by SessionBytes) does not expose the negotiated ALPN protocol. Only SSLEngine.getApplicationProtocol does.
  • SSLEngine.setHandshakeApplicationProtocolSelector is only meaningful for the peer that selects the protocol, i.e. the server.
  • HandshakeCompletedListener is SSLSocket-only; SSLEngine has no equivalent.

Proposed fix

Wrap the engine returned by HttpsConnectionContext.engineCreator in a delegating SSLEngine that, on the first wrap/unwrap after which getApplicationProtocol goes non-null, completes a Promise[String]. The client-side switch stage installs the chosen layer from an AsyncCallback hung off that promise.

Notes on shape:

  • Per-materialization state is required — PersistentConnection.managedConnection re-materializes the connection flow on every reconnect, so the httpsWithAlpn approach of a closed-over var plus an "is not reusable" guard is not viable here. Building the stack inside Flow.fromMaterializer gives a fresh promise and engine per materialization.
  • The switch stage must buffer inbound elements that arrive before the async callback is delivered (with h2, the server's SETTINGS frame can land first) and replay them on install.
  • Types line up favourably: Http.ClientLayer (Http.scala:794) is BidiFlow[HttpRequest, SslTlsOutbound, SslTlsInbound, HttpResponse, NotUsed], and Http2Blueprint.clientStack (Http2Blueprint.scala:146) .atop(unwrapTls) (:327) has that same shape. Both carry NotUsed, so the client switch needs none of the ServerTerminator plumbing ProtocolSwitch carries — but it is a 4-port BidiFlow stage rather than the server's 2-port Flow.

The delegation is ~30 mechanical methods, which is the main cost of this approach.

Alternative considered

A TimerGraphStageLogic polling engine.getApplicationProtocol is a few lines instead of a delegating engine, but puts a poll loop in the connection setup path. Recorded here as the fallback if the wrapper turns out to interact badly with the TLS stage.

Prerequisite bug

Http2JDKAlpnSupport.clientSetApplicationProtocols (Http2AlpnSupport.scala:74) ignores its protocols parameter and hardcodes Array("h2"):

def clientSetApplicationProtocols(engine: SSLEngine, protocols: Array[String]): Unit = {
  val params = engine.getSSLParameters
  params.setApplicationProtocols(Array("h2"))   // should be `protocols`
  engine.setSSLParameters(params)
}

Harmless today because the only caller (Http2.scala:275) passes Array("h2"), but this is exactly the seam negotiation needs — offering ["h2", "http/1.1"] is step one.

主要言語
Scala
スター
196
フォーク
56
平均マージ
1日 18時間
マージ済み PR(30日)
43

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

apache/pekko-http のほかの issue

apache/pekko-http の issue をすべて見る

似ている issue

Scala の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。