From 558a7ebc713c8b95133c4903ea94ec9160d3e757 Mon Sep 17 00:00:00 2001 From: Tyler Gregory Date: Fri, 1 Aug 2025 13:11:41 -0500 Subject: [PATCH 1/6] Do not set keep-alive on non-http-1-0 connections Add a mechanism for getting a HttpCall's HTTP version. Update query param encoding test Fix rebase Appease projectHealth check Make SocketAddress from method internal Fix port conflict Fix JettyHealthService Switch to UriCompliance.LEGACY Fix tests Fix Http2ConnectivityTest fix JettyServletUpstreamResponse fix JettyService fix imports first pass upgrading deps and updating code --- build.gradle.kts | 1 + gradle/libs.versions.toml | 17 ++-- .../misk/grpc/GrpcReflectTestingModule.kt | 2 +- misk-testing/api/misk-testing.api | 41 ++++---- misk-testing/build.gradle.kts | 1 - .../src/main/kotlin/misk/web/FakeHttpCall.kt | 2 + misk/api/misk.api | 20 ++++ misk/build.gradle.kts | 7 +- misk/src/main/kotlin/misk/web/HttpCall.kt | 15 +++ .../main/kotlin/misk/web/ServletHttpCall.kt | 7 +- .../extractors/ResponseBodyFeatureBinding.kt | 6 +- .../main/kotlin/misk/web/http/HttpVersion.kt | 20 ++++ .../misk/web/jetty/JettyHealthService.kt | 14 ++- .../kotlin/misk/web/jetty/JettyService.kt | 94 +++++++++---------- .../web/jetty/JettyServletUpstreamResponse.kt | 8 +- .../kotlin/misk/web/jetty/JettyWebSocket.kt | 18 ++-- .../misk/web/jetty/WebActionsServlet.kt | 29 ++---- .../kotlin/misk/web/actions/SseActionTest.kt | 1 - .../RequestDeadlineInterceptorTest.kt | 4 +- .../misk/web/jetty/WebActionsServletTest.kt | 23 ++++- .../misk/web/proxy/WebProxyActionTest.kt | 2 + .../misk/web/ssl/Http2ConnectivityTest.kt | 6 +- 22 files changed, 208 insertions(+), 130 deletions(-) create mode 100644 misk/src/main/kotlin/misk/web/http/HttpVersion.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6d726b1fb3d..bb0a13a7033 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -265,6 +265,7 @@ subprojects { add("api", platform(rootProject.libs.jacksonBom)) add("api", platform(rootProject.libs.jerseyBom)) add("api", platform(rootProject.libs.jettyBom)) + add("api", platform(rootProject.libs.jettyEe8Bom)) add("api", platform(rootProject.libs.kotlinBom)) add("api", platform(rootProject.libs.nettyBom)) add("api", platform(rootProject.libs.prometheusClientBom)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99da38e6d7b..9fa2456742b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ googleHttp = "2.0.0" guava = "33.4.8-jre" hoplite = "2.7.5" jackson = "2.19.2" +jetty = "12.0.23" junit = "5.13.4" kotest = "5.9.1" kotlin = "2.1.20" @@ -124,19 +125,20 @@ jerseyBom = { module = "org.glassfish.jersey:jersey-bom", version = "3.1.11" } jetbrainsAnnotations = { module = "org.jetbrains:annotations", version = "26.0.2" } jettyAlpnServer = { module = "org.eclipse.jetty:jetty-alpn-server" } jettyAlpnServerJava = { module = "org.eclipse.jetty:jetty-alpn-java-server" } -jettyBom = { module = "org.eclipse.jetty:jetty-bom", version = "10.0.24" } +jettyBom = { module = "org.eclipse.jetty:jetty-bom", version.ref = "jetty" } +jettyEe8Bom = { module = "org.eclipse.jetty.ee8:jetty-ee8-bom", version.ref = "jetty" } +jettyEe8Nested = { module = "org.eclipse.jetty.ee8:jetty-ee8-nested" } jettyHttp = { module = "org.eclipse.jetty:jetty-http" } -jettyHttp2 = { module = "org.eclipse.jetty.http2:http2-server" } +jettyHttp2 = { module = "org.eclipse.jetty.http2:jetty-http2-server" } jettyIo = { module = "org.eclipse.jetty:jetty-io" } jettyServer = { module = "org.eclipse.jetty:jetty-server" } -jettyServlet = { module = "org.eclipse.jetty:jetty-servlet" } +jettyServlet = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlet" } jettyServletApi = { module = "org.eclipse.jetty.toolchain:jetty-servlet-api", version = "4.0.6" } -jettyServlets = { module = "org.eclipse.jetty:jetty-servlets" } +jettyServlets = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlets" } jettyUds = { module = "org.eclipse.jetty:jetty-unixdomain-server" } -jettyUnixSocket = { module = "org.eclipse.jetty:jetty-unixsocket-server" } jettyUtil = { module = "org.eclipse.jetty:jetty-util" } -jettyWebsocketApi = { module = "org.eclipse.jetty.websocket:websocket-jetty-api" } -jettyWebsocketServer = { module = "org.eclipse.jetty.websocket:websocket-jetty-server" } +jettyWebsocketApiEE8 = { module = "org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-api" } +jettyWebsocketServerEE8 = { module = "org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server" } jnrUnixsocket = { module = "com.github.jnr:jnr-unixsocket", version = "0.38.23" } jooq = { module = "org.jooq:jooq", version = "3.19.18" } jsqlparser = { module = "com.github.jsqlparser:jsqlparser", version = "5.3" } @@ -209,7 +211,6 @@ retrofitMoshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref retrofitProtobuf = { module = "com.squareup.retrofit2:converter-protobuf", version.ref = "retrofit" } retrofitScalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "retrofit" } retrofitWire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } -servletApi = { module = "javax.servlet:javax.servlet-api", version = "4.0.1" } slf4jApi = { module = "org.slf4j:slf4j-api", version = "2.0.17" } sqldelightJdbcDriver = { module = "app.cash.sqldelight:jdbc-driver", version.ref = "sqldelight" } sqldelightMysqlDialect = { module = "app.cash.sqldelight:mysql-dialect", version.ref = "sqldelight" } diff --git a/misk-grpc-reflect/src/test/kotlin/misk/grpc/GrpcReflectTestingModule.kt b/misk-grpc-reflect/src/test/kotlin/misk/grpc/GrpcReflectTestingModule.kt index de579da2465..3ccd494c158 100644 --- a/misk-grpc-reflect/src/test/kotlin/misk/grpc/GrpcReflectTestingModule.kt +++ b/misk-grpc-reflect/src/test/kotlin/misk/grpc/GrpcReflectTestingModule.kt @@ -19,7 +19,7 @@ import misk.web.jetty.JettyService import okhttp3.HttpUrl class GrpcReflectTestingModule : KAbstractModule() { - val webConfig = WebTestingModule.TESTING_WEB_CONFIG.copy(port = 9090) + val webConfig = WebTestingModule.TESTING_WEB_CONFIG.copy(port = 9999) override fun configure() { install(WebTestingModule(webConfig = webConfig)) diff --git a/misk-testing/api/misk-testing.api b/misk-testing/api/misk-testing.api index 71d007c7344..6c0ea5476d5 100644 --- a/misk-testing/api/misk-testing.api +++ b/misk-testing/api/misk-testing.api @@ -343,40 +343,43 @@ public final class misk/web/FakeHttpCall : misk/web/HttpCall { public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;)V public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;I)V public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;II)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;Z)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;)V - public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;)V - public synthetic fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;Z)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;)V + public fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;)V + public synthetic fun (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun accepts ()Ljava/util/List; public fun addResponseHeaders (Lokhttp3/Headers;)V public fun asOkHttpRequest ()Lokhttp3/Request; public final fun component1 ()Lokhttp3/HttpUrl; - public final fun component10 ()Lokio/BufferedSource; - public final fun component11 ()Lokio/BufferedSink; - public final fun component12 ()Lmisk/web/actions/WebSocket; - public final fun component13 ()Lmisk/web/actions/WebSocketListener; - public final fun component14 ()Ljava/util/List; + public final fun component10 ()Lokhttp3/Headers$Builder; + public final fun component11 ()Lokio/BufferedSource; + public final fun component12 ()Lokio/BufferedSink; + public final fun component13 ()Lmisk/web/actions/WebSocket; + public final fun component14 ()Lmisk/web/actions/WebSocketListener; + public final fun component15 ()Ljava/util/List; public final fun component2 ()Lmisk/web/SocketAddress; public final fun component3 ()Lmisk/web/DispatchMechanism; public final fun component4 ()Lokhttp3/Headers; public final fun component5 ()I public final fun component6 ()I - public final fun component7 ()Lokhttp3/Headers$Builder; - public final fun component8 ()Z - public final fun component9 ()Lokhttp3/Headers$Builder; + public final fun component7 ()Lmisk/web/http/HttpVersion; + public final fun component8 ()Lokhttp3/Headers$Builder; + public final fun component9 ()Z public fun computeRequestHeader (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public fun contentType ()Lokhttp3/MediaType; - public final fun copy (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;)Lmisk/web/FakeHttpCall; - public static synthetic fun copy$default (Lmisk/web/FakeHttpCall;Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;ILjava/lang/Object;)Lmisk/web/FakeHttpCall; + public final fun copy (Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;)Lmisk/web/FakeHttpCall; + public static synthetic fun copy$default (Lmisk/web/FakeHttpCall;Lokhttp3/HttpUrl;Lmisk/web/SocketAddress;Lmisk/web/DispatchMechanism;Lokhttp3/Headers;IILmisk/web/http/HttpVersion;Lokhttp3/Headers$Builder;ZLokhttp3/Headers$Builder;Lokio/BufferedSource;Lokio/BufferedSink;Lmisk/web/actions/WebSocket;Lmisk/web/actions/WebSocketListener;Ljava/util/List;ILjava/lang/Object;)Lmisk/web/FakeHttpCall; public fun equals (Ljava/lang/Object;)Z public fun getCookies ()Ljava/util/List; public fun getDispatchMechanism ()Lmisk/web/DispatchMechanism; public final fun getHeadersBuilder ()Lokhttp3/Headers$Builder; + public fun getHttpVersion ()Lmisk/web/http/HttpVersion; public fun getLinkLayerLocalAddress ()Lmisk/web/SocketAddress; public fun getNetworkStatusCode ()I public final fun getRequestBody ()Lokio/BufferedSource; diff --git a/misk-testing/build.gradle.kts b/misk-testing/build.gradle.kts index 375c56908e0..e9cf37ccf97 100644 --- a/misk-testing/build.gradle.kts +++ b/misk-testing/build.gradle.kts @@ -18,7 +18,6 @@ dependencies { api(libs.moshiCore) api(libs.okHttp) api(libs.openTracingMock) - api(libs.servletApi) api(libs.logbackClassic) api(project(":misk")) api(project(":misk-actions")) diff --git a/misk-testing/src/main/kotlin/misk/web/FakeHttpCall.kt b/misk-testing/src/main/kotlin/misk/web/FakeHttpCall.kt index eadc47e3532..f80bd7e0851 100644 --- a/misk-testing/src/main/kotlin/misk/web/FakeHttpCall.kt +++ b/misk-testing/src/main/kotlin/misk/web/FakeHttpCall.kt @@ -2,6 +2,7 @@ package misk.web import misk.web.actions.WebSocket import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl @@ -18,6 +19,7 @@ data class FakeHttpCall @JvmOverloads constructor( override var requestHeaders: Headers = headersOf(), override var statusCode: Int = 200, override var networkStatusCode: Int = 200, + override val httpVersion: HttpVersion = HttpVersion.HTTP_1_1, val headersBuilder: Headers.Builder = Headers.Builder(), var sendTrailers: Boolean = false, val trailersBuilder: Headers.Builder = Headers.Builder(), diff --git a/misk/api/misk.api b/misk/api/misk.api index 09922a66bca..b253c313f93 100644 --- a/misk/api/misk.api +++ b/misk/api/misk.api @@ -1348,6 +1348,7 @@ public abstract interface class misk/web/HttpCall : misk/api/HttpRequest { public abstract fun computeRequestHeader (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V public abstract fun contentType ()Lokhttp3/MediaType; public abstract fun getCookies ()Ljava/util/List; + public abstract fun getHttpVersion ()Lmisk/web/http/HttpVersion; public abstract fun getLinkLayerLocalAddress ()Lmisk/web/SocketAddress; public abstract fun getNetworkStatusCode ()I public abstract fun getRequestReceivedTimestamp ()J @@ -1520,6 +1521,10 @@ public final class misk/web/ResponseExtensionsKt { } public abstract class misk/web/SocketAddress { + public static final field Companion Lmisk/web/SocketAddress$Companion; +} + +public final class misk/web/SocketAddress$Companion { } public final class misk/web/SocketAddress$Network : misk/web/SocketAddress { @@ -1923,6 +1928,21 @@ public final class misk/web/formatter/ClassNameFormatter$Companion { public final fun format (Lkotlin/reflect/KClass;)Ljava/lang/String; } +public final class misk/web/http/HttpVersion : java/lang/Enum { + public static final field Companion Lmisk/web/http/HttpVersion$Companion; + public static final field HTTP_0_9 Lmisk/web/http/HttpVersion; + public static final field HTTP_1_0 Lmisk/web/http/HttpVersion; + public static final field HTTP_1_1 Lmisk/web/http/HttpVersion; + public static final field HTTP_2_0 Lmisk/web/http/HttpVersion; + public static final field HTTP_3_0 Lmisk/web/http/HttpVersion; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lmisk/web/http/HttpVersion; + public static fun values ()[Lmisk/web/http/HttpVersion; +} + +public final class misk/web/http/HttpVersion$Companion { +} + public final class misk/web/interceptors/ActionLoggingConfig { public static final field Companion Lmisk/web/interceptors/ActionLoggingConfig$Companion; public fun ()V diff --git a/misk/build.gradle.kts b/misk/build.gradle.kts index 30f699d0012..d55158b37f3 100644 --- a/misk/build.gradle.kts +++ b/misk/build.gradle.kts @@ -24,7 +24,6 @@ dependencies { api(libs.openTracing) api(libs.prometheusClient) api(libs.retrofit) - api(libs.servletApi) api(libs.slf4jApi) api(project(":misk-action-scopes")) api(project(":misk-actions")) @@ -44,15 +43,15 @@ dependencies { api(project(":wisp:wisp-deployment")) implementation(libs.jCommander) implementation(libs.jettyAlpnServer) + implementation(libs.jettyEe8Nested) implementation(libs.jettyHttp) implementation(libs.jettyHttp2) implementation(libs.jettyIo) implementation(libs.jettyServlet) implementation(libs.jettyServlets) implementation(libs.jettyUds) - implementation(libs.jettyUnixSocket) - implementation(libs.jettyWebsocketApi) - implementation(libs.jettyWebsocketServer) + implementation(libs.jettyWebsocketApiEE8) + implementation(libs.jettyWebsocketServerEE8) implementation(libs.jnrUnixsocket) implementation(libs.kotlinReflect) implementation(libs.kotlinStdLibJdk8) diff --git a/misk/src/main/kotlin/misk/web/HttpCall.kt b/misk/src/main/kotlin/misk/web/HttpCall.kt index 49e25081da9..3010f6491de 100644 --- a/misk/src/main/kotlin/misk/web/HttpCall.kt +++ b/misk/src/main/kotlin/misk/web/HttpCall.kt @@ -3,6 +3,7 @@ package misk.web import misk.api.HttpRequest import misk.web.actions.WebSocket import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import misk.web.mediatype.MediaRange import okhttp3.Headers import okhttp3.MediaType @@ -11,6 +12,8 @@ import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import okio.BufferedSink import okio.BufferedSource +import java.net.InetSocketAddress +import java.net.UnixDomainSocketAddress import javax.servlet.http.Cookie /** @@ -19,6 +22,16 @@ import javax.servlet.http.Cookie sealed class SocketAddress { class Network(val ipAddress: String, val port: Int) : SocketAddress() class Unix(val path: String) : SocketAddress() + + companion object { + internal fun from(javaSocketAddress: java.net.SocketAddress): SocketAddress { + return when (javaSocketAddress) { + is InetSocketAddress -> Network(javaSocketAddress.address.hostAddress, javaSocketAddress.port) + is UnixDomainSocketAddress -> Unix(javaSocketAddress.path.toString()) + else -> throw IllegalArgumentException("Unknown SocketAddress type ${javaSocketAddress.javaClass.simpleName}") + } + } + } } /** @@ -44,6 +57,8 @@ interface HttpCall: HttpRequest { /** Timestamp when the request was received (milliseconds since epoch) */ val requestReceivedTimestamp: Long + val httpVersion: HttpVersion + /** Set both the raw network status code and the meaningful status code that's * recorded in metrics */ fun setStatusCodes(statusCode: Int, networkStatusCode: Int) diff --git a/misk/src/main/kotlin/misk/web/ServletHttpCall.kt b/misk/src/main/kotlin/misk/web/ServletHttpCall.kt index 0ad8cc607d9..73309b067ea 100644 --- a/misk/src/main/kotlin/misk/web/ServletHttpCall.kt +++ b/misk/src/main/kotlin/misk/web/ServletHttpCall.kt @@ -2,13 +2,14 @@ package misk.web import misk.web.actions.WebSocket import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import misk.web.jetty.headers import misk.web.jetty.httpUrl import okhttp3.Headers import okhttp3.HttpUrl import okio.BufferedSink import okio.BufferedSource -import org.eclipse.jetty.server.Request +import org.eclipse.jetty.ee8.nested.Request import javax.servlet.http.Cookie import javax.servlet.http.HttpServletRequest @@ -44,6 +45,9 @@ internal data class ServletHttpCall( override val responseHeaders: Headers get() = upstreamResponse.headers + override val httpVersion: HttpVersion + get() = upstreamResponse.httpVersion + override fun setStatusCodes(statusCode: Int, networkStatusCode: Int) { _actualStatusCode = statusCode upstreamResponse.statusCode = networkStatusCode @@ -106,6 +110,7 @@ internal data class ServletHttpCall( interface UpstreamResponse { var statusCode: Int val headers: Headers + val httpVersion: HttpVersion fun setHeader(name: String, value: String) fun addHeaders(headers: Headers) fun requireTrailers() diff --git a/misk/src/main/kotlin/misk/web/extractors/ResponseBodyFeatureBinding.kt b/misk/src/main/kotlin/misk/web/extractors/ResponseBodyFeatureBinding.kt index edfb16dde5b..92f956a99ae 100644 --- a/misk/src/main/kotlin/misk/web/extractors/ResponseBodyFeatureBinding.kt +++ b/misk/src/main/kotlin/misk/web/extractors/ResponseBodyFeatureBinding.kt @@ -17,6 +17,7 @@ import misk.web.PathPattern import misk.web.ResponseSink import misk.web.ResponseSinkChannel import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import misk.web.interceptors.ResponseBodyMarshallerFactory import misk.web.marshal.Marshaller import misk.web.mediatype.MediaTypes @@ -54,7 +55,10 @@ internal class ResponseBodyFeatureBinding( with(subject.httpCall) { setResponseHeader("Content-Type", MediaTypes.SERVER_EVENT_STREAM) setResponseHeader("Cache-Control", "no-cache") - setResponseHeader("Connection", "keep-alive") + // Keep-Alive is an HTTP/1.0 mechanism only + if (subject.httpCall.httpVersion == HttpVersion.HTTP_1_0) { + setResponseHeader("Connection", "keep-alive") + } setResponseHeader("X-Accel-Buffering", "no") } } diff --git a/misk/src/main/kotlin/misk/web/http/HttpVersion.kt b/misk/src/main/kotlin/misk/web/http/HttpVersion.kt new file mode 100644 index 00000000000..8a51422fdce --- /dev/null +++ b/misk/src/main/kotlin/misk/web/http/HttpVersion.kt @@ -0,0 +1,20 @@ +package misk.web.http + +enum class HttpVersion { + HTTP_0_9, + HTTP_1_0, + HTTP_1_1, + HTTP_2_0, + HTTP_3_0; + + companion object { + internal fun fromJetty(version: org.eclipse.jetty.http.HttpVersion): HttpVersion = + when (version) { + org.eclipse.jetty.http.HttpVersion.HTTP_0_9 -> HTTP_0_9 + org.eclipse.jetty.http.HttpVersion.HTTP_1_0 -> HTTP_1_0 + org.eclipse.jetty.http.HttpVersion.HTTP_1_1 -> HTTP_1_1 + org.eclipse.jetty.http.HttpVersion.HTTP_2 -> HTTP_2_0 + org.eclipse.jetty.http.HttpVersion.HTTP_3 -> HTTP_3_0 + } + } +} \ No newline at end of file diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt b/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt index 41f1e5a1467..7d2a6c055f5 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt @@ -8,18 +8,19 @@ import misk.annotation.ExperimentalMiskApi import misk.web.WebConfig import mu.KLogger import okhttp3.HttpUrl +import org.eclipse.jetty.ee8.servlet.ServletContextHandler +import org.eclipse.jetty.ee8.servlet.ServletHolder +import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer import org.eclipse.jetty.http.UriCompliance import org.eclipse.jetty.io.ConnectionStatistics +import org.eclipse.jetty.server.Handler import org.eclipse.jetty.server.HttpConfiguration import org.eclipse.jetty.server.HttpConnectionFactory import org.eclipse.jetty.server.NetworkConnector import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.handler.StatisticsHandler -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.servlet.ServletHolder import org.eclipse.jetty.util.thread.ExecutorThreadPool -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer import misk.logging.getLogger import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadPoolExecutor @@ -100,7 +101,7 @@ internal class JettyHealthService @Inject internal constructor( private fun setupHttpConnector() { val httpConnectionFactory = HttpConnectionFactory( HttpConfiguration().apply { - uriCompliance = UriCompliance.RFC3986 + uriCompliance = UriCompliance.LEGACY sendServerVersion = false setFormEncodedMethods() }) @@ -144,7 +145,10 @@ internal class JettyHealthService @Inject internal constructor( JettyWebSocketServletContainerInitializer.configure(servletContextHandler, null) server.addManaged(servletContextHandler) - statisticsHandler.handler = servletContextHandler + val handlers = Handler.Sequence().apply { + addHandler(servletContextHandler) + } + statisticsHandler.handler = handlers } private fun setupServer() { diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt index a9574682e40..43e42afd76b 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt @@ -17,13 +17,21 @@ import misk.web.jetty.JettyHealthService.Companion.jettyHealthServiceEnabled import misk.web.mediatype.MediaTypes import okhttp3.HttpUrl import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory +import org.eclipse.jetty.ee8.nested.HandlerWrapper +import org.eclipse.jetty.ee8.servlet.FilterHolder +import org.eclipse.jetty.ee8.servlet.ServletContextHandler +import org.eclipse.jetty.ee8.servlet.ServletHolder +import org.eclipse.jetty.ee8.servlets.CrossOriginFilter +import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer import org.eclipse.jetty.http.UriCompliance +import org.eclipse.jetty.http.pathmap.PathSpec import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory import org.eclipse.jetty.io.ConnectionStatistics import org.eclipse.jetty.server.ConnectionFactory import org.eclipse.jetty.server.Connector +import org.eclipse.jetty.server.Handler import org.eclipse.jetty.server.HttpConfiguration import org.eclipse.jetty.server.HttpConnectionFactory import org.eclipse.jetty.server.NetworkConnector @@ -34,16 +42,10 @@ import org.eclipse.jetty.server.SslConnectionFactory import org.eclipse.jetty.server.handler.ContextHandler import org.eclipse.jetty.server.handler.StatisticsHandler import org.eclipse.jetty.server.handler.gzip.GzipHandler -import org.eclipse.jetty.servlet.FilterHolder -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.servlet.ServletHolder -import org.eclipse.jetty.servlets.CrossOriginFilter import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector -import org.eclipse.jetty.unixsocket.server.UnixSocketConnector import org.eclipse.jetty.util.JavaVersion import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.util.thread.ThreadPool -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer import misk.logging.getLogger import java.io.File import java.io.IOException @@ -108,7 +110,7 @@ class JettyService @Inject internal constructor( val httpConnectionFactories = mutableListOf() val httpConfig = HttpConfiguration() httpConfig.customizeForGrpc() - httpConfig.uriCompliance = UriCompliance.RFC3986 + httpConfig.uriCompliance = UriCompliance.LEGACY httpConfig.sendServerVersion = false if (webConfig.ssl != null) { httpConfig.securePort = webConfig.ssl.port @@ -258,49 +260,34 @@ class JettyService @Inject internal constructor( udsConnFactories.add(HTTP2CServerConnectionFactory(httpConfig)) } - if (isJEP380Supported(socketConfig.path)) { - logger.info("Using UnixDomainServerConnector for ${socketConfig.path}") - val udsConnector = UnixDomainServerConnector( - server, - null /* executor */, - null /* scheduler */, - null /* buffer pool */, - webConfig.acceptors ?: -1, - webConfig.selectors ?: -1, - *udsConnFactories.toTypedArray() - ) - val socketFile = File(socketConfig.path) - udsConnector.unixDomainPath = socketFile.toPath() - udsConnector.addBean(connectionMetricsCollector.newConnectionListener("http", 0)) - udsConnector.name = "uds" - - // try to clean up any leftover socket files before connecting - if (socketFile.exists() && !socketFile.delete()) { - logger.warn("Could not delete file $socketFile") - } + logger.info("Using UnixDomainServerConnector for ${socketConfig.path}") + val udsConnector = UnixDomainServerConnector( + server, + null /* executor */, + null /* scheduler */, + null /* buffer pool */, + webConfig.acceptors ?: -1, + webConfig.selectors ?: -1, + *udsConnFactories.toTypedArray() + ) + val socketFile = File(socketConfig.path) + udsConnector.unixDomainPath = socketFile.toPath() + udsConnector.addBean(connectionMetricsCollector.newConnectionListener("http", 0)) + udsConnector.name = "uds" + + // try to clean up any leftover socket files before connecting + if (socketFile.exists() && !socketFile.delete()) { + logger.warn("Could not delete file $socketFile") + } - // set file permissions after socket creation so sidecars (e.g. envoy, istio) have access - try { - udsConnector.start() - setFilePermissions(socketFile) - } catch (e: Exception) { - cleanAndThrow(udsConnector, e) - } - server.addConnector(udsConnector) - } else { - val udsConnector = UnixSocketConnector( - server, - null /* executor */, - null /* scheduler */, - null /* buffer pool */, - webConfig.selectors ?: -1, - *udsConnFactories.toTypedArray() - ) - udsConnector.setUnixSocket(socketConfig.path) - udsConnector.addBean(connectionMetricsCollector.newConnectionListener("http", 0)) - udsConnector.name = "uds" - server.addConnector(udsConnector) + // set file permissions after socket creation so sidecars (e.g. envoy, istio) have access + try { + udsConnector.start() + setFilePermissions(socketFile) + } catch (e: Exception) { + cleanAndThrow(udsConnector, e) } + server.addConnector(udsConnector) } // TODO(mmihic): Force security handler? @@ -310,7 +297,10 @@ class JettyService @Inject internal constructor( JettyWebSocketServletContainerInitializer.configure(servletContextHandler, null) server.addManaged(servletContextHandler) - statisticsHandler.handler = servletContextHandler + val handlers = Handler.Sequence().apply { + addHandler(servletContextHandler) + } + statisticsHandler.handler = handlers statisticsHandler.server = server // Kubernetes sends a SIG_TERM and gives us 30 seconds to stop gracefully. @@ -451,11 +441,11 @@ private val Server.httpsUrl: HttpUrl? } internal fun NetworkConnector.toHttpUrl(): HttpUrl { - val context = server.getChildHandlerByClass(ContextHandler::class.java) + val context = server.getDescendant(ContextHandler::class.java) val protocol = defaultConnectionFactory.protocol val scheme = if (protocol.startsWith("SSL-") || protocol == "SSL") "https" else "http" - val virtualHosts = context?.virtualHosts ?: arrayOf() + val virtualHosts = context?.virtualHosts ?: emptyList() val explicitHost = if (virtualHosts.isEmpty()) host else virtualHosts[0] return HttpUrl.Builder() @@ -508,7 +498,7 @@ private fun setFilePermissions(file: File) { private fun cleanAndThrow(connector: Connector, exception: Exception) { val runtimeException = RuntimeException(exception) - if (connector.isStarted()) { + if (connector.isStarted) { try { connector.stop() } catch (e: Exception) { diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt b/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt index 7f445946986..83c480e8799 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt @@ -2,10 +2,11 @@ package misk.web.jetty import misk.web.ServletHttpCall import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import okhttp3.Headers import okhttp3.Headers.Companion.headersOf +import org.eclipse.jetty.ee8.nested.Response import org.eclipse.jetty.http.HttpFields -import org.eclipse.jetty.server.Response import java.util.function.Supplier internal class JettyServletUpstreamResponse( @@ -23,6 +24,9 @@ internal class JettyServletUpstreamResponse( override val headers: Headers get() = response.headers() + override val httpVersion: HttpVersion + get() = HttpVersion.fromJetty(response.httpChannel.request.httpVersion) + override fun setHeader(name: String, value: String) { response.setHeader(name, value) } @@ -37,7 +41,7 @@ internal class JettyServletUpstreamResponse( sendTrailers = true // Set the callback that'll return trailers at the end of the response body. - response.trailers = Supplier { + response.trailers = Supplier { val httpFields = HttpFields.build() for (i in 0 until trailers.size) { httpFields.add(trailers.name(i), trailers.value(i)) diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt b/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt index d40ce203d57..9fe5594fe79 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt @@ -6,16 +6,17 @@ import misk.web.ServletHttpCall import misk.web.actions.WebAction import misk.web.actions.WebSocket import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import okhttp3.Headers import okio.ByteString import okio.ByteString.Companion.toByteString import okio.utf8Size -import org.eclipse.jetty.websocket.api.Session -import org.eclipse.jetty.websocket.api.WebSocketAdapter -import org.eclipse.jetty.websocket.api.WriteCallback -import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse -import org.eclipse.jetty.websocket.server.JettyWebSocketCreator +import org.eclipse.jetty.ee8.websocket.api.Session +import org.eclipse.jetty.ee8.websocket.api.WebSocketAdapter +import org.eclipse.jetty.ee8.websocket.api.WriteCallback +import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeRequest +import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeResponse +import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketCreator import java.util.ArrayDeque private const val MAX_QUEUE_SIZE = 16 * 1024 * 1024 @@ -35,7 +36,7 @@ internal class JettyWebSocket( private var listener: WebSocketListener? = null private val adapter = object : WebSocketAdapter() { - override fun onWebSocketConnect(sess: Session?) { + override fun onWebSocketConnect(sess: Session) { super.onWebSocketConnect(sess) sendQueue() } @@ -65,6 +66,9 @@ internal class JettyWebSocket( response.statusCode = value } + override val httpVersion: HttpVersion + get() = error("No http version for websocket responses") + override val headers: Headers get() = response.headers() diff --git a/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt b/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt index 3891a000191..9eac4e31b1c 100644 --- a/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt +++ b/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt @@ -20,16 +20,15 @@ import okio.BufferedSink import okio.buffer import okio.sink import okio.source +import org.eclipse.jetty.ee8.nested.Request +import org.eclipse.jetty.ee8.nested.Response +import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeResponse +import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet +import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServletFactory import org.eclipse.jetty.http.BadMessageException import org.eclipse.jetty.http.HttpMethod -import org.eclipse.jetty.server.Request -import org.eclipse.jetty.server.Response import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector -import org.eclipse.jetty.unixsocket.server.UnixSocketConnector -import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse -import org.eclipse.jetty.websocket.server.JettyWebSocketServlet -import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory import misk.logging.getLogger import java.net.HttpURLConnection import java.net.ProtocolException @@ -132,19 +131,9 @@ internal class WebActionsServlet @Inject constructor( val httpCall = ServletHttpCall.create( request = request, linkLayerLocalAddress = with((request as? Request)?.httpChannel) { - when (this?.connector) { - is UnixDomainServerConnector -> SocketAddress.Unix( - (this.connector as UnixDomainServerConnector).unixDomainPath.toString() - ) - - is UnixSocketConnector -> SocketAddress.Unix( - (this.connector as UnixSocketConnector).unixSocket - ) - - is ServerConnector -> SocketAddress.Network( - this.endPoint.remoteAddress.address.hostAddress, - (this.connector as ServerConnector).localPort - ) + when (this?.connectionMetaData?.connector) { + is UnixDomainServerConnector, + is ServerConnector -> SocketAddress.from(this.connectionMetaData.localSocketAddress) else -> throw IllegalStateException("Unknown socket connector.") } @@ -242,7 +231,7 @@ internal fun HttpServletRequest.headers(): Headers { return result.build() } -internal fun HttpServletResponse.headers(): Headers { +internal fun Response.headers(): Headers { val result = Headers.Builder() for (name in headerNames) { for (value in getHeaders(name)) { diff --git a/misk/src/test/kotlin/misk/web/actions/SseActionTest.kt b/misk/src/test/kotlin/misk/web/actions/SseActionTest.kt index 56d254dabe3..14be60bc040 100644 --- a/misk/src/test/kotlin/misk/web/actions/SseActionTest.kt +++ b/misk/src/test/kotlin/misk/web/actions/SseActionTest.kt @@ -259,7 +259,6 @@ class SseActionTest { assertThat(response.code).isEqualTo(200) assertThat(response.header("Content-Type")).isEqualTo("text/event-stream") assertThat(response.header("Cache-Control")).isEqualTo("no-cache") - assertThat(response.header("Connection")).isEqualTo("keep-alive") assertThat(response.header("X-Accel-Buffering")).isEqualTo("no") } diff --git a/misk/src/test/kotlin/misk/web/interceptors/RequestDeadlineInterceptorTest.kt b/misk/src/test/kotlin/misk/web/interceptors/RequestDeadlineInterceptorTest.kt index c456939c74d..a838f0b641d 100644 --- a/misk/src/test/kotlin/misk/web/interceptors/RequestDeadlineInterceptorTest.kt +++ b/misk/src/test/kotlin/misk/web/interceptors/RequestDeadlineInterceptorTest.kt @@ -30,6 +30,7 @@ import java.time.Clock import java.time.Duration import java.time.Instant import jakarta.inject.Inject +import misk.web.http.HttpVersion @MiskTest(startService = false) class RequestDeadlineInterceptorTest { @@ -475,7 +476,8 @@ class RequestDeadlineInterceptorTest { // Simple fake UpstreamResponse for testing internal class FakeUpstreamResponse( override var statusCode: Int = 200, - override val headers: Headers = headersOf() + override val headers: Headers = headersOf(), + override val httpVersion: HttpVersion = HttpVersion.HTTP_1_1 ) : ServletHttpCall.UpstreamResponse { private val headersBuilder = Headers.Builder() private val trailersMap = mutableMapOf() diff --git a/misk/src/test/kotlin/misk/web/jetty/WebActionsServletTest.kt b/misk/src/test/kotlin/misk/web/jetty/WebActionsServletTest.kt index fcc099aca98..cc3901a83d7 100644 --- a/misk/src/test/kotlin/misk/web/jetty/WebActionsServletTest.kt +++ b/misk/src/test/kotlin/misk/web/jetty/WebActionsServletTest.kt @@ -62,16 +62,28 @@ class WebActionsServletTest { } @Test - fun malformedUriQueryParamsResponseDoesNotContainStacktrace() { + fun incompleteUtf8EncodingDoesNotFail() { val response = get( path = "/potato", viaUDS = false, viaFileUDS = false, - encodedQuery = "test" to "%3C%a%3C", + encodedQuery = "test" to "%C1%BF", + ) + + assertThat(response.body.string()).isEqualTo("TestActionResponse(text=foo)") + assertThat(response.code).isEqualTo(200) + } + + @Test + fun invalidPathCharactersReturns400() { + val response = get( + path = "/potato%00bb", + viaUDS = false, + viaFileUDS = false, ) - assertThat(response.body.string()).isEqualTo("400: Unable to parse URI query") assertThat(response.code).isEqualTo(400) + assertThat(response.body.string()).contains("HTTP ERROR 400 Bad Request") } @Test @@ -95,7 +107,7 @@ class WebActionsServletTest { .url(jettyService.httpServerUrl.newBuilder().encodedPath("/fooasdf/").build()) .patch("bar".toRequestBody()) ) - assertThat(response.body?.string()).contains("Nothing found at PATCH", "fooasdf") + assertThat(response.body.string()).contains("Nothing found at PATCH", "fooasdf") } internal class WebActionsServletNetworkInterceptor : NetworkInterceptor { @@ -114,10 +126,11 @@ class WebActionsServletTest { ) .build() ) + chain.proceed(chain.httpCall) } class Factory : NetworkInterceptor.Factory { - override fun create(action: Action): NetworkInterceptor? = + override fun create(action: Action): NetworkInterceptor = WebActionsServletNetworkInterceptor() } } diff --git a/misk/src/test/kotlin/misk/web/proxy/WebProxyActionTest.kt b/misk/src/test/kotlin/misk/web/proxy/WebProxyActionTest.kt index e069aa0bb9c..3bd1c687633 100644 --- a/misk/src/test/kotlin/misk/web/proxy/WebProxyActionTest.kt +++ b/misk/src/test/kotlin/misk/web/proxy/WebProxyActionTest.kt @@ -383,6 +383,8 @@ class WebProxyActionTest { } } + // TODO Permitting this necessitates usage of UriCompliance.LEGACY in misk's HttpConfig. + // We should either make this configurable or switch to a strict compliance mode @Test internal fun getForwardedSlashesOnSlashes() { upstreamServer.enqueue( diff --git a/misk/src/test/kotlin/misk/web/ssl/Http2ConnectivityTest.kt b/misk/src/test/kotlin/misk/web/ssl/Http2ConnectivityTest.kt index 114cbea0b41..462a2dc509a 100644 --- a/misk/src/test/kotlin/misk/web/ssl/Http2ConnectivityTest.kt +++ b/misk/src/test/kotlin/misk/web/ssl/Http2ConnectivityTest.kt @@ -265,7 +265,8 @@ class Http2ConnectivityTest { @ResponseContentType(MediaTypes.TEXT_PLAIN_UTF8) fun disconnect(): Response { val request = actionScopedServletRequest.get() as org.eclipse.jetty.server.Request - request.httpChannel.abort(Exception("boom")) // Synthesize a connectivity failure. + request.connectionMetaData.connection.onClose(Exception("boom")) // Synthesize a connectivity failure. + request.connectionMetaData.connection.close() return Response(body = "") } @@ -279,7 +280,8 @@ class Http2ConnectivityTest { @ResponseContentType(MediaTypes.TEXT_PLAIN_UTF8) fun disconnect(): ResponseBody { val request = actionScopedServletRequest.get() as org.eclipse.jetty.server.Request - request.httpChannel.abort(Exception("boom")) // Synthesize a connectivity failure. + request.connectionMetaData.connection.onClose(Exception("boom")) // Synthesize a connectivity failure. + request.connectionMetaData.connection.close() return object : ResponseBody { override fun writeTo(sink: BufferedSink) { From 09bb358e0936124596148d7af75cbcbe0d787c41 Mon Sep 17 00:00:00 2001 From: Tyler Gregory Date: Tue, 2 Jun 2026 12:02:58 -0500 Subject: [PATCH 2/6] Post-merge updates --- build.gradle.kts | 36 ++++++++-------- misk/api/misk.api | 6 +-- .../main/kotlin/misk/web/http/HttpVersion.kt | 42 ++++++++++++------- .../jetty/GenericServletUpstreamResponse.kt | 6 ++- .../kotlin/misk/web/jetty/JettyService.kt | 9 ---- .../web/jetty/MeasuredWindowRateControl.kt | 4 +- .../misk/web/jetty/WebActionsServlet.kt | 17 +++++--- 7 files changed, 67 insertions(+), 53 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 33695635b12..123949fdc1c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -131,7 +131,7 @@ apiValidation { "exemplar", "exemplarchat", "detektive", - "misk-schema-migrator-gradle-plugin" + "misk-schema-migrator-gradle-plugin", ) ignoredProjects.addAll(subprojects.map { it.name }.filter { it in ignorable }) additionalSourceSets.add("testFixtures") @@ -189,13 +189,13 @@ val hibernateProjects = listOf( "misk-jdbc-testing", "misk-hibernate-testing", "misk-rate-limiting-bucket4j-mysql", - "misk-sqldelight" + "misk-sqldelight", ) val redisProjects = listOf( "misk-redis", "misk-redis-lettuce", - "misk-rate-limiting-bucket4j-redis" + "misk-rate-limiting-bucket4j-redis", ) val detektConfig = file("detekt.yaml") @@ -252,12 +252,12 @@ subprojects { plugins.withType { tasks.withType().configureEach { compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - freeCompilerArgs.add("-Xjdk-release=11") + jvmTarget.set(JvmTarget.JVM_17) + freeCompilerArgs.add("-Xjdk-release=17") } } tasks.withType().configureEach { - options.release.set(11) + options.release.set(17) } dependencies { @@ -312,7 +312,7 @@ subprojects { "dd.civisibility.git.upload.enabled" to false, "dd.integration.opentracing.enabled" to true, "dd.instrumentation.telemetry.enabled" to false, - ) + ), ) develocity.testRetry { maxRetries.set(1) @@ -378,7 +378,7 @@ subprojects { if (name in configurationNames) { attributes.attribute( Usage.USAGE_ATTRIBUTE, - this@subprojects.objects.named(Usage::class, Usage.JAVA_RUNTIME) + this@subprojects.objects.named(Usage::class, Usage.JAVA_RUNTIME), ) } @@ -467,7 +467,7 @@ abstract class StartRedisTask @Inject constructor( val portIsOccupied = try { Socket("localhost", redisPort).close() true - } catch (e: IOException) { + } catch (_: IOException) { false } if (portIsOccupied) { @@ -484,7 +484,7 @@ abstract class StartRedisTask @Inject constructor( "-p", "$redisPort:6379", redisImage, "redis-server", - "--loglevel debug" + "--loglevel debug", ) execOperations.exec { workingDir(rootDir.get().asFile) @@ -518,7 +518,7 @@ abstract class StartRedisClusterTask @Inject constructor( val portIsOccupied = try { Socket("localhost", redisSeedPort).close() true - } catch (e: IOException) { + } catch (_: IOException) { false } if (portIsOccupied) { @@ -537,26 +537,28 @@ abstract class StartRedisClusterTask @Inject constructor( "-e", "MASTERS=3", "-e", "SLAVES_PER_MASTER=1", "-p", "7000-7005:7000-7005", - redisImage + redisImage, ) execOperations.exec { workingDir(rootDir.get().asFile) commandLine(*dockerArguments) } - waitForRedisCluster(redisContainerName,redisSeedPort) + waitForRedisCluster(redisContainerName, redisSeedPort) logger.info("Started Redis Cluster docker image $redisImage on port $redisSeedPort") } - private fun waitForRedisCluster(containerName:String, port:Int){ + private fun waitForRedisCluster(containerName: String, port: Int) { println("Waiting for Redis cluster to become available...") val deadline = System.currentTimeMillis() + 60.seconds.inWholeMilliseconds fun clusterReady(): Boolean { try { - val process = ProcessBuilder("docker", "exec", containerName, - "redis-cli", "-c", "-p", port.toString(), "cluster", "info") + val process = ProcessBuilder( + "docker", "exec", containerName, + "redis-cli", "-c", "-p", port.toString(), "cluster", "info", + ) .redirectErrorStream(true) .start() @@ -564,7 +566,7 @@ abstract class StartRedisClusterTask @Inject constructor( process.waitFor(5, TimeUnit.SECONDS) return "cluster_state:ok" in output && "slots_assigned:16384" in output - } catch (e: Exception) { + } catch (_: Exception) { return false } } diff --git a/misk/api/misk.api b/misk/api/misk.api index 8880037f9c2..042bb73eca0 100644 --- a/misk/api/misk.api +++ b/misk/api/misk.api @@ -2076,14 +2076,14 @@ public final class misk/web/jetty/MeasuredThreadPoolExecutor : misk/web/jetty/Me public fun queueSize ()I } -public final class misk/web/jetty/MeasuredWindowRateControl : org/eclipse/jetty/http2/parser/RateControl { +public final class misk/web/jetty/MeasuredWindowRateControl : org/eclipse/jetty/http2/RateControl { public synthetic fun (ILmisk/metrics/v2/PeakGauge;Lio/prometheus/client/Counter;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun onEvent (Ljava/lang/Object;)Z } -public final class misk/web/jetty/MeasuredWindowRateControl$Factory : org/eclipse/jetty/http2/parser/RateControl$Factory { +public final class misk/web/jetty/MeasuredWindowRateControl$Factory : org/eclipse/jetty/http2/RateControl$Factory { public fun (Lmisk/metrics/v2/Metrics;Lmisk/web/WebConfig;)V - public fun newRateControl (Lorg/eclipse/jetty/io/EndPoint;)Lorg/eclipse/jetty/http2/parser/RateControl; + public fun newRateControl (Lorg/eclipse/jetty/io/EndPoint;)Lorg/eclipse/jetty/http2/RateControl; } public final class misk/web/jetty/ThreadPoolQueueMetrics { diff --git a/misk/src/main/kotlin/misk/web/http/HttpVersion.kt b/misk/src/main/kotlin/misk/web/http/HttpVersion.kt index 8a51422fdce..5259a0471cd 100644 --- a/misk/src/main/kotlin/misk/web/http/HttpVersion.kt +++ b/misk/src/main/kotlin/misk/web/http/HttpVersion.kt @@ -1,20 +1,30 @@ package misk.web.http enum class HttpVersion { - HTTP_0_9, - HTTP_1_0, - HTTP_1_1, - HTTP_2_0, - HTTP_3_0; + HTTP_0_9, + HTTP_1_0, + HTTP_1_1, + HTTP_2_0, + HTTP_3_0; - companion object { - internal fun fromJetty(version: org.eclipse.jetty.http.HttpVersion): HttpVersion = - when (version) { - org.eclipse.jetty.http.HttpVersion.HTTP_0_9 -> HTTP_0_9 - org.eclipse.jetty.http.HttpVersion.HTTP_1_0 -> HTTP_1_0 - org.eclipse.jetty.http.HttpVersion.HTTP_1_1 -> HTTP_1_1 - org.eclipse.jetty.http.HttpVersion.HTTP_2 -> HTTP_2_0 - org.eclipse.jetty.http.HttpVersion.HTTP_3 -> HTTP_3_0 - } - } -} \ No newline at end of file + companion object { + internal fun fromServletRequestProtocol(protocol: String) = + when (protocol) { + "HTTP/0.9" -> HTTP_0_9 + "HTTP/1.0" -> HTTP_1_0 + "HTTP/1.1" -> HTTP_1_1 + "HTTP/2.0" -> HTTP_2_0 + "HTTP/3.0" -> HTTP_3_0 + else -> error("Unrecognized protocol: $protocol") + } + + internal fun fromJetty(version: org.eclipse.jetty.http.HttpVersion): HttpVersion = + when (version) { + org.eclipse.jetty.http.HttpVersion.HTTP_0_9 -> HTTP_0_9 + org.eclipse.jetty.http.HttpVersion.HTTP_1_0 -> HTTP_1_0 + org.eclipse.jetty.http.HttpVersion.HTTP_1_1 -> HTTP_1_1 + org.eclipse.jetty.http.HttpVersion.HTTP_2 -> HTTP_2_0 + org.eclipse.jetty.http.HttpVersion.HTTP_3 -> HTTP_3_0 + } + } +} diff --git a/misk/src/main/kotlin/misk/web/jetty/GenericServletUpstreamResponse.kt b/misk/src/main/kotlin/misk/web/jetty/GenericServletUpstreamResponse.kt index 36439948ad0..48193722ae7 100644 --- a/misk/src/main/kotlin/misk/web/jetty/GenericServletUpstreamResponse.kt +++ b/misk/src/main/kotlin/misk/web/jetty/GenericServletUpstreamResponse.kt @@ -3,6 +3,7 @@ package misk.web.jetty import javax.servlet.http.HttpServletResponse import misk.web.ServletHttpCall import misk.web.actions.WebSocketListener +import misk.web.http.HttpVersion import okhttp3.Headers import okhttp3.Headers.Companion.headersOf @@ -10,7 +11,7 @@ import okhttp3.Headers.Companion.headersOf * A generic implementation of ServletHttpCall.UpstreamResponse that works with standard HttpServletResponse instead of * requiring Jetty's specific Response class. */ -internal class GenericServletUpstreamResponse(private val response: HttpServletResponse) : +internal class GenericServletUpstreamResponse(private val protocol: String, private val response: HttpServletResponse) : ServletHttpCall.UpstreamResponse { private var sendTrailers = false private var trailers = headersOf() @@ -24,6 +25,9 @@ internal class GenericServletUpstreamResponse(private val response: HttpServletR override val headers: Headers get() = response.headers() + override val httpVersion: HttpVersion + get() = HttpVersion.fromServletRequestProtocol(protocol) + override fun setHeader(name: String, value: String) { response.setHeader(name, value) } diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt index b316994f088..8a84f4cf062 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt @@ -56,7 +56,6 @@ import org.eclipse.jetty.server.handler.StatisticsHandler import org.eclipse.jetty.server.handler.gzip.GzipHandler import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector import org.eclipse.jetty.util.JavaVersion -import org.eclipse.jetty.util.MultiException import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.util.thread.ThreadPool @@ -362,10 +361,6 @@ internal constructor( // distinguished from a regular unix socket by the fact that the first byte of // the address is a null byte ('\0'). The address has no connection with filesystem // path names. - } catch (e: MultiException) { - // Jetty wraps multiple InvalidPathExceptions into a MultiException when stopping - // multiple abstract unix domain sockets (addresses starting with '\0'). - if (!isOnlyInvalidPathExceptions(e)) throw e } logger.info { "Stopped Jetty in $stopwatch" } @@ -455,10 +450,6 @@ private fun AbstractHTTP2ServerConnectionFactory.customize(webConfig: WebConfig) } } -private fun isOnlyInvalidPathExceptions(e: MultiException): Boolean { - return e.throwables.isNotEmpty() && e.throwables.all { it is InvalidPathException } -} - /** * JEP-380 is supported when running Java 16+ and the provided socket path is non-abstract. Abstract socket paths are * identified by paths prefixed with an `@` symbol or a null byte. diff --git a/misk/src/main/kotlin/misk/web/jetty/MeasuredWindowRateControl.kt b/misk/src/main/kotlin/misk/web/jetty/MeasuredWindowRateControl.kt index 9eddcc87122..91c6b9929e8 100644 --- a/misk/src/main/kotlin/misk/web/jetty/MeasuredWindowRateControl.kt +++ b/misk/src/main/kotlin/misk/web/jetty/MeasuredWindowRateControl.kt @@ -21,13 +21,13 @@ import java.util.concurrent.atomic.AtomicInteger import misk.metrics.v2.Metrics import misk.metrics.v2.PeakGauge import misk.web.WebConfig -import org.eclipse.jetty.http2.parser.RateControl +import org.eclipse.jetty.http2.RateControl import org.eclipse.jetty.io.EndPoint import org.eclipse.jetty.util.NanoTime /** * Misk's RateControl implementation with observability for monitoring HTTP/2 frame rate limiting. Almost the same - * implementation as [org.eclipse.jetty.http2.parser.WindowRateControl]. + * implementation as [org.eclipse.jetty.http2.WindowRateControl]. */ class MeasuredWindowRateControl private constructor( diff --git a/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt b/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt index fcad8ed6d57..eeaca122445 100644 --- a/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt +++ b/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt @@ -140,7 +140,7 @@ constructor( if (response is Response) { JettyServletUpstreamResponse(response) } else { - GenericServletUpstreamResponse(response) + GenericServletUpstreamResponse(request.protocol, response) }, requestBody = request.inputStream.source().buffer(), responseBody = responseBody, @@ -250,6 +250,16 @@ internal fun HttpServletRequest.headers(): Headers { return result.build() } +internal fun HttpServletResponse.headers(): Headers { + val result = Headers.Builder() + for (name in headerNames) { + for (value in getHeaders(name)) { + result.addUnsafeNonAscii(name, value) + } + } + return result.build() +} + internal fun Response.headers(): Headers { val result = Headers.Builder() for (name in headerNames) { @@ -297,10 +307,7 @@ private fun extractLinkLayerLocalAddress(request: HttpServletRequest): SocketAdd return when (connector) { is UnixDomainServerConnector -> SocketAddress.Unix(connector.unixDomainPath.toString()) - is UnixSocketConnector -> SocketAddress.Unix(connector.unixSocket) - - is ServerConnector -> - SocketAddress.Network(httpChannel.endPoint.remoteAddress.address.hostAddress, connector.localPort) + is ServerConnector -> SocketAddress.Network(httpChannel.remoteAddress.address.hostAddress, connector.localPort) else -> throw IllegalStateException("Unknown socket connector.") } From bf2d1a9798ef65e33d6ae6620bd2f33508d64eaa Mon Sep 17 00:00:00 2001 From: Tyler Gregory Date: Wed, 17 Jun 2026 11:58:25 -0500 Subject: [PATCH 3/6] Migrate to Jetty ee9 --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 12 +++---- misk/build.gradle.kts | 14 ++++---- .../main/kotlin/misk/web/ServletHttpCall.kt | 2 +- .../misk/web/jetty/JettyHealthService.kt | 6 ++-- .../kotlin/misk/web/jetty/JettyService.kt | 36 +++++++++---------- .../web/jetty/JettyServletUpstreamResponse.kt | 2 +- .../kotlin/misk/web/jetty/JettyWebSocket.kt | 12 +++---- .../misk/web/jetty/WebActionsServlet.kt | 10 +++--- .../kotlin/misk/web/jetty/JettyServiceTest.kt | 20 +++++------ 10 files changed, 57 insertions(+), 59 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 123949fdc1c..895d665398b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -272,7 +272,7 @@ subprojects { add("api", platform(rootProject.libs.jacksonBom)) add("api", platform(rootProject.libs.jerseyBom)) add("api", platform(rootProject.libs.jettyBom)) - add("api", platform(rootProject.libs.jettyEe8Bom)) + add("api", platform(rootProject.libs.jettyEe9Bom)) add("api", platform(rootProject.libs.kotlinBom)) add("api", platform(rootProject.libs.nettyBom)) add("api", platform(rootProject.libs.prometheusClientBom)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7bebe976740..115c3506458 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -137,20 +137,20 @@ jetbrainsAnnotations = { module = "org.jetbrains:annotations", version = "26.0.2 jettyAlpnServer = { module = "org.eclipse.jetty:jetty-alpn-server" } jettyAlpnServerJava = { module = "org.eclipse.jetty:jetty-alpn-java-server" } jettyBom = { module = "org.eclipse.jetty:jetty-bom", version.ref = "jetty" } -jettyEe8Bom = { module = "org.eclipse.jetty.ee8:jetty-ee8-bom", version.ref = "jetty" } -jettyEe8Nested = { module = "org.eclipse.jetty.ee8:jetty-ee8-nested" } +jettyEe9Bom = { module = "org.eclipse.jetty.ee9:jetty-ee9-bom", version.ref = "jetty" } +jettyEe9Nested = { module = "org.eclipse.jetty.ee9:jetty-ee9-nested" } jettyHttp = { module = "org.eclipse.jetty:jetty-http" } jettyHttp2 = { module = "org.eclipse.jetty.http2:jetty-http2-server" } jettyHttp2Common = { module = "org.eclipse.jetty.http2:jetty-http2-common" } jettyIo = { module = "org.eclipse.jetty:jetty-io" } jettyServer = { module = "org.eclipse.jetty:jetty-server" } -jettyServlet = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlet" } +jettyServlet = { module = "org.eclipse.jetty.ee9:jetty-ee9-servlet" } jettyServletApi = { module = "org.eclipse.jetty.toolchain:jetty-jakarta-servlet-api", version = "5.0.2" } -jettyServlets = { module = "org.eclipse.jetty.ee8:jetty-ee8-servlets" } +jettyServlets = { module = "org.eclipse.jetty.ee9:jetty-ee9-servlets" } jettyUds = { module = "org.eclipse.jetty:jetty-unixdomain-server" } jettyUtil = { module = "org.eclipse.jetty:jetty-util" } -jettyWebsocketApiEE8 = { module = "org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-api" } -jettyWebsocketServerEE8 = { module = "org.eclipse.jetty.ee8.websocket:jetty-ee8-websocket-jetty-server" } +jettyWebsocketApiEE9 = { module = "org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-api" } +jettyWebsocketServerEE9 = { module = "org.eclipse.jetty.ee9.websocket:jetty-ee9-websocket-jetty-server" } jnrUnixsocket = { module = "com.github.jnr:jnr-unixsocket", version = "0.38.24" } jooq = { module = "org.jooq:jooq" } jooqBom = { module = "org.jooq:jooq-bom", version.ref = "jooq" } diff --git a/misk/build.gradle.kts b/misk/build.gradle.kts index 63266f71e28..75921315626 100644 --- a/misk/build.gradle.kts +++ b/misk/build.gradle.kts @@ -47,14 +47,14 @@ dependencies { api(project(":misk-feature")) implementation(libs.jCommander) implementation(libs.jettyAlpnServer) - implementation(libs.jettyEe8Nested) + implementation(libs.jettyEe9Nested) implementation(libs.jettyHttp) implementation(libs.jettyHttp2) implementation(libs.jettyServlet) implementation(libs.jettyServlets) implementation(libs.jettyUds) - implementation(libs.jettyWebsocketApiEE8) - implementation(libs.jettyWebsocketServerEE8) + implementation(libs.jettyWebsocketApiEE9) + implementation(libs.jettyWebsocketServerEE9) implementation(libs.jnrUnixsocket) implementation(libs.kotlinReflect) implementation(libs.kotlinStdLibJdk8) @@ -119,7 +119,7 @@ wire { rpcCallStyle = "blocking" exclusive = false includes = listOf( - "helloworld.Greeter" + "helloworld.Greeter", ) } @@ -130,7 +130,7 @@ wire { exclusive = false singleMethodServices = true includes = listOf( - "helloworld.Greeter" + "helloworld.Greeter", ) } } @@ -149,7 +149,7 @@ afterEvaluate { } kotlinSourceSets?.getByName("main")?.kotlin?.setSrcDirs( - kotlinSourceSets.getByName("main").kotlin.srcDirs.filter { !it.path.contains(generatedSourceDir) } + kotlinSourceSets.getByName("main").kotlin.srcDirs.filter { !it.path.contains(generatedSourceDir) }, ) kotlinSourceSets?.getByName("test")?.kotlin?.srcDir(generatedSourceDir) @@ -160,6 +160,6 @@ afterEvaluate { mavenPublishing { configure( - KotlinJvm(javadocJar = Dokka("dokkaGfm")) + KotlinJvm(javadocJar = Dokka("dokkaGfm")), ) } diff --git a/misk/src/main/kotlin/misk/web/ServletHttpCall.kt b/misk/src/main/kotlin/misk/web/ServletHttpCall.kt index 8fb453c1b7a..eaf2e5d8e30 100644 --- a/misk/src/main/kotlin/misk/web/ServletHttpCall.kt +++ b/misk/src/main/kotlin/misk/web/ServletHttpCall.kt @@ -11,7 +11,7 @@ import okhttp3.Headers import okhttp3.HttpUrl import okio.BufferedSink import okio.BufferedSource -import org.eclipse.jetty.ee8.nested.Request +import org.eclipse.jetty.ee9.nested.Request internal data class ServletHttpCall( override val url: HttpUrl, diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt b/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt index 151c6bde06d..8e2f18549ad 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyHealthService.kt @@ -14,9 +14,9 @@ import misk.logging.getLogger import misk.web.WebConfig import mu.KLogger import okhttp3.HttpUrl -import org.eclipse.jetty.ee8.servlet.ServletContextHandler -import org.eclipse.jetty.ee8.servlet.ServletHolder -import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer +import org.eclipse.jetty.ee9.servlet.ServletContextHandler +import org.eclipse.jetty.ee9.servlet.ServletHolder +import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer import org.eclipse.jetty.http.UriCompliance import org.eclipse.jetty.io.ConnectionStatistics import org.eclipse.jetty.server.Handler diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt index af9e82eca71..dc4e8c24094 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt @@ -31,11 +31,11 @@ import misk.web.jetty.JettyHealthService.Companion.jettyHealthServiceEnabled import misk.web.mediatype.MediaTypes import okhttp3.HttpUrl import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory -import org.eclipse.jetty.ee8.servlet.FilterHolder -import org.eclipse.jetty.ee8.servlet.ServletContextHandler -import org.eclipse.jetty.ee8.servlet.ServletHolder -import org.eclipse.jetty.ee8.servlets.CrossOriginFilter -import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer +import org.eclipse.jetty.ee9.servlet.FilterHolder +import org.eclipse.jetty.ee9.servlet.ServletContextHandler +import org.eclipse.jetty.ee9.servlet.ServletHolder +import org.eclipse.jetty.ee9.servlets.CrossOriginFilter +import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer import org.eclipse.jetty.http.UriCompliance import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory @@ -105,7 +105,7 @@ internal constructor( server, healthExecutor, null, /* scheduler */ - null /* buffer pool */, + null, /* buffer pool */ 1, 1, HttpConnectionFactory(), @@ -149,9 +149,9 @@ internal constructor( val httpConnector = ServerConnector( server, - null /* executor */, - null /* scheduler */, - null /* buffer pool */, + null, /* executor */ + null, /* scheduler */ + null, /* buffer pool */ webConfig.acceptors ?: -1, webConfig.selectors ?: -1, *httpConnectionFactories.toTypedArray(), @@ -230,9 +230,9 @@ internal constructor( val httpsConnector = ServerConnector( server, - null /* executor */, - null /* scheduler */, - null /* buffer pool */, + null, /* executor */ + null, /* scheduler */ + null, /* buffer pool */ webConfig.acceptors ?: -1, webConfig.selectors ?: -1, *httpsConnectionFactories.toTypedArray(), @@ -269,9 +269,9 @@ internal constructor( val udsConnector = UnixDomainServerConnector( server, - null /* executor */, - null /* scheduler */, - null /* buffer pool */, + null, /* executor */ + null, /* scheduler */ + null, /* buffer pool */ webConfig.acceptors ?: -1, webConfig.selectors ?: -1, *udsConnFactories.toTypedArray(), @@ -402,17 +402,17 @@ internal constructor( private val Server.healthUrl: HttpUrl? get() { - return connectors.mapNotNull { it as? NetworkConnector }.firstOrNull { it.name == "health" }?.toHttpUrl() + return connectors.filterIsInstance().firstOrNull { it.name == "health" }?.toHttpUrl() } private val Server.httpUrl: HttpUrl? get() { - return connectors.mapNotNull { it as? NetworkConnector }.firstOrNull { it.name == "http" }?.toHttpUrl() + return connectors.filterIsInstance().firstOrNull { it.name == "http" }?.toHttpUrl() } private val Server.httpsUrl: HttpUrl? get() { - return connectors.mapNotNull { it as? NetworkConnector }.firstOrNull { it.name == "https" }?.toHttpUrl() + return connectors.filterIsInstance().firstOrNull { it.name == "https" }?.toHttpUrl() } internal fun NetworkConnector.toHttpUrl(): HttpUrl { diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt b/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt index efe1f3dbce3..175753b9b04 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyServletUpstreamResponse.kt @@ -6,7 +6,7 @@ import misk.web.actions.WebSocketListener import misk.web.http.HttpVersion import okhttp3.Headers import okhttp3.Headers.Companion.headersOf -import org.eclipse.jetty.ee8.nested.Response +import org.eclipse.jetty.ee9.nested.Response import org.eclipse.jetty.http.HttpFields internal class JettyServletUpstreamResponse(val response: Response) : ServletHttpCall.UpstreamResponse { diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt b/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt index 17a69132ec9..29e889e1496 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyWebSocket.kt @@ -12,12 +12,12 @@ import okhttp3.Headers import okio.ByteString import okio.ByteString.Companion.toByteString import okio.utf8Size -import org.eclipse.jetty.ee8.websocket.api.Session -import org.eclipse.jetty.ee8.websocket.api.WebSocketAdapter -import org.eclipse.jetty.ee8.websocket.api.WriteCallback -import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeRequest -import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeResponse -import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketCreator +import org.eclipse.jetty.ee9.websocket.api.Session +import org.eclipse.jetty.ee9.websocket.api.WebSocketAdapter +import org.eclipse.jetty.ee9.websocket.api.WriteCallback +import org.eclipse.jetty.ee9.websocket.server.JettyServerUpgradeRequest +import org.eclipse.jetty.ee9.websocket.server.JettyServerUpgradeResponse +import org.eclipse.jetty.ee9.websocket.server.JettyWebSocketCreator private const val MAX_QUEUE_SIZE = 16 * 1024 * 1024 diff --git a/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt b/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt index 32c40fd3d14..9ac878899e1 100644 --- a/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt +++ b/misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt @@ -25,11 +25,11 @@ import okio.BufferedSink import okio.buffer import okio.sink import okio.source -import org.eclipse.jetty.ee8.nested.Request -import org.eclipse.jetty.ee8.nested.Response -import org.eclipse.jetty.ee8.websocket.server.JettyServerUpgradeResponse -import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet -import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServletFactory +import org.eclipse.jetty.ee9.nested.Request +import org.eclipse.jetty.ee9.nested.Response +import org.eclipse.jetty.ee9.websocket.server.JettyServerUpgradeResponse +import org.eclipse.jetty.ee9.websocket.server.JettyWebSocketServlet +import org.eclipse.jetty.ee9.websocket.server.JettyWebSocketServletFactory import org.eclipse.jetty.http.BadMessageException import org.eclipse.jetty.http.HttpMethod import org.eclipse.jetty.server.ServerConnector diff --git a/misk/src/test/kotlin/misk/web/jetty/JettyServiceTest.kt b/misk/src/test/kotlin/misk/web/jetty/JettyServiceTest.kt index 7989c40236b..f380d933904 100644 --- a/misk/src/test/kotlin/misk/web/jetty/JettyServiceTest.kt +++ b/misk/src/test/kotlin/misk/web/jetty/JettyServiceTest.kt @@ -9,7 +9,6 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.handler.StatisticsHandler import org.eclipse.jetty.server.handler.gzip.GzipHandler -import org.eclipse.jetty.util.MultiException import org.eclipse.jetty.util.thread.ThreadPool import org.junit.jupiter.api.Test import org.mockito.Mockito.doThrow @@ -29,14 +28,13 @@ class JettyServiceTest { } @Test - fun `stop suppresses MultiException when all nested exceptions are InvalidPathException`() { + fun `stop suppresses exception when all nested exceptions are InvalidPathException`() { val server = mock(Server::class.java) - val multi = MultiException() - multi.add(InvalidPathException("http-ingress.sock", "Nul character not allowed")) - multi.add(InvalidPathException("istio-proxy.sock", "Nul character not allowed")) - multi.add(InvalidPathException("grpc-ingress.sock", "Nul character not allowed")) + val exception = InvalidPathException("http-ingress.sock", "Nul character not allowed") + exception.addSuppressed(InvalidPathException("istio-proxy.sock", "Nul character not allowed")) + exception.addSuppressed(InvalidPathException("grpc-ingress.sock", "Nul character not allowed")) `when`(server.isRunning).thenReturn(true) - doThrow(multi).`when`(server).stop() + doThrow(exception).`when`(server).stop() val jettyService = jettyService(server) @@ -44,11 +42,11 @@ class JettyServiceTest { } @Test - fun `stop rethrows MultiException when shutdown failures include non InvalidPathException`() { + fun `stop rethrows exception when shutdown failures include non InvalidPathException`() { val server = mock(Server::class.java) - val multi = MultiException() - multi.add(InvalidPathException("http-ingress.sock", "Nul character not allowed")) - multi.add(RuntimeException("unexpected shutdown failure")) + val multi = RuntimeException() + multi.addSuppressed(InvalidPathException("http-ingress.sock", "Nul character not allowed")) + multi.addSuppressed(RuntimeException("unexpected shutdown failure")) `when`(server.isRunning).thenReturn(true) doThrow(multi).`when`(server).stop() From 4fc424551ead1d1d5d630244094d4e19d2735ad6 Mon Sep 17 00:00:00 2001 From: Kartikaya Gupta Date: Thu, 18 Jun 2026 10:26:49 -0400 Subject: [PATCH 4/6] Set version refs on jetty12 artifacts --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 115c3506458..9322300b59f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -140,8 +140,8 @@ jettyBom = { module = "org.eclipse.jetty:jetty-bom", version.ref = "jetty" } jettyEe9Bom = { module = "org.eclipse.jetty.ee9:jetty-ee9-bom", version.ref = "jetty" } jettyEe9Nested = { module = "org.eclipse.jetty.ee9:jetty-ee9-nested" } jettyHttp = { module = "org.eclipse.jetty:jetty-http" } -jettyHttp2 = { module = "org.eclipse.jetty.http2:jetty-http2-server" } -jettyHttp2Common = { module = "org.eclipse.jetty.http2:jetty-http2-common" } +jettyHttp2 = { module = "org.eclipse.jetty.http2:jetty-http2-server", version.ref = "jetty" } +jettyHttp2Common = { module = "org.eclipse.jetty.http2:jetty-http2-common", version.ref = "jetty" } jettyIo = { module = "org.eclipse.jetty:jetty-io" } jettyServer = { module = "org.eclipse.jetty:jetty-server" } jettyServlet = { module = "org.eclipse.jetty.ee9:jetty-ee9-servlet" } From e866d39e8947d42918220ad58da802681383deb1 Mon Sep 17 00:00:00 2001 From: Kartikaya Gupta Date: Fri, 31 Jul 2026 21:17:49 -0400 Subject: [PATCH 5/6] Set fallback server authority for Unix-domain connectors on Jetty 12 Jetty 12 derives the request's server authority from the connection's local address when a request carries no Host/:authority (ConnectionMetaData.getServerAuthority falls back to getLocalSocketAddress). For a Unix domain socket that address is the socket file path (e.g. /etc/sockets/istio-proxy.sock), which HostPort rejects as an invalid authority: it logs "Bad Authority: [...]" and throws, failing the request with a 400. This bites authority-less requests such as the HTTP/2 prior-knowledge preface ("PRI * HTTP/2.0"), which HttpStreamOverHTTP1 resolves before the h2c upgrade -- and h2c defaults to true for UDS sockets. It also bites any HTTP/1.0 request without a Host header. The failure happens while building the request URI, before request customizers run, so HostHeaderCustomizer cannot fix it; setting the fallback authority on HttpConfiguration is the supported knob. Set serverAuthority on a copy of the shared HttpConfiguration, scoped to the Unix-domain connectors, so TCP connectors keep deriving their authority from the (perfectly valid) local host:port. Requests that do carry a Host/:authority continue to use their own value. This mirrors the equivalent fix in Cash's service-container connector factories and JSC's GenericServerConfigurator. --- .../kotlin/misk/web/jetty/JettyService.kt | 14 ++- .../misk/web/uds/UDSServerAuthorityTest.kt | 86 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt diff --git a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt index dc4e8c24094..8da6a3ad6d5 100644 --- a/misk/src/main/kotlin/misk/web/jetty/JettyService.kt +++ b/misk/src/main/kotlin/misk/web/jetty/JettyService.kt @@ -55,6 +55,7 @@ import org.eclipse.jetty.server.handler.ContextHandler import org.eclipse.jetty.server.handler.StatisticsHandler import org.eclipse.jetty.server.handler.gzip.GzipHandler import org.eclipse.jetty.unixdomain.server.UnixDomainServerConnector +import org.eclipse.jetty.util.HostPort import org.eclipse.jetty.util.JavaVersion import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.util.thread.ThreadPool @@ -257,10 +258,19 @@ internal constructor( socketConfigs.addAll(webConfig.unix_domain_sockets) } socketConfigs.stream().forEach() { socketConfig -> + // Provide a fallback server authority for the Unix-domain connector. Jetty 12 derives the + // server authority from the connection's local address when a request carries no Host/ + // :authority (e.g. the bare "PRI * HTTP/2.0" HTTP/2 prior-knowledge preface). For a Unix + // domain socket that local address is the socket file path, which HostPort rejects as an + // invalid authority ("Bad Authority"). Requests that do carry a Host/:authority still use + // their own value; this is only a fallback. + val udsHttpConfig = HttpConfiguration(httpConfig) + udsHttpConfig.serverAuthority = HostPort("localhost") + val udsConnFactories = mutableListOf() - udsConnFactories.add(HttpConnectionFactory(httpConfig)) + udsConnFactories.add(HttpConnectionFactory(udsHttpConfig)) if (socketConfig.h2c == true) { - val http2 = HTTP2CServerConnectionFactory(httpConfig) + val http2 = HTTP2CServerConnectionFactory(udsHttpConfig) http2.rateControlFactory = http2RateControlFactory udsConnFactories.add(http2) } diff --git a/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt b/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt new file mode 100644 index 00000000000..94db725aef9 --- /dev/null +++ b/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt @@ -0,0 +1,86 @@ +package misk.web.uds + +import jakarta.inject.Inject +import java.net.StandardProtocolFamily +import java.net.UnixDomainSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.SocketChannel +import java.nio.file.Files +import misk.MiskTestingServiceModule +import misk.inject.KAbstractModule +import misk.testing.MiskTest +import misk.testing.MiskTestModule +import misk.web.Get +import misk.web.ResponseContentType +import misk.web.WebActionModule +import misk.web.WebServerTestingModule +import misk.web.WebUnixDomainSocketConfig +import misk.web.actions.WebAction +import misk.web.jetty.JettyService +import misk.web.mediatype.MediaTypes +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Jetty 12 derives a request's server authority from the connection's local address when the request carries no + * Host/:authority. On a Unix domain socket that local address is the socket file path, which + * [org.eclipse.jetty.util.HostPort] rejects as an invalid authority -- it logs "Bad Authority" and throws, failing the + * request with a 400 before it ever reaches a web action. + * + * [JettyService] avoids that by setting a fallback server authority on the Unix-domain connector's HttpConfiguration. + */ +@MiskTest(startService = true) +class UDSServerAuthorityTest { + @MiskTestModule val module = TestModule() + + @Inject private lateinit var jetty: JettyService + + /** An HTTP/1.0 request is valid without a Host header, so Jetty must fall back to the authority. */ + @Test + fun `request without a Host header is served`() { + val response = sendRaw("GET /hello HTTP/1.0\r\n\r\n") + + assertThat(response).startsWith("HTTP/1.1 200") + assertThat(response).endsWith("hello") + } + + private fun sendRaw(request: String): String { + SocketChannel.open(StandardProtocolFamily.UNIX).use { channel -> + channel.connect(UnixDomainSocketAddress.of(socketPath)) + channel.write(ByteBuffer.wrap(request.toByteArray(Charsets.US_ASCII))) + + val response = StringBuilder() + val buffer = ByteBuffer.allocate(1024) + while (channel.read(buffer) != -1) { + buffer.flip() + response.append(Charsets.US_ASCII.decode(buffer)) + buffer.clear() + } + return response.toString() + } + } + + class HelloAction @Inject constructor() : WebAction { + @Get("/hello") @ResponseContentType(MediaTypes.TEXT_PLAIN_UTF8) fun sayHello() = "hello" + } + + inner class TestModule : KAbstractModule() { + override fun configure() { + install( + WebServerTestingModule( + webConfig = + WebServerTestingModule.TESTING_WEB_CONFIG.copy( + unix_domain_sockets = listOf(WebUnixDomainSocketConfig(path = socketPath)) + ) + ) + ) + install(MiskTestingServiceModule()) + install(WebActionModule.create()) + } + } + + companion object { + // Keep this short: Unix domain socket paths are capped near 104 bytes on macOS. + private val socketPath = Files.createTempDirectory("uds").resolve("authority.sock").toString() + } +} From 57e7c826ac92b469e5c9743c6fc6740b38680a7e Mon Sep 17 00:00:00 2001 From: Kartikaya Gupta Date: Fri, 31 Jul 2026 21:18:39 -0400 Subject: [PATCH 6/6] Drop unused JettyService injection from UDSServerAuthorityTest @MiskTest(startService = true) already starts the service; the field was never read. --- misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt b/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt index 94db725aef9..046c46587aa 100644 --- a/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt +++ b/misk/src/test/kotlin/misk/web/uds/UDSServerAuthorityTest.kt @@ -33,8 +33,6 @@ import org.junit.jupiter.api.Test class UDSServerAuthorityTest { @MiskTestModule val module = TestModule() - @Inject private lateinit var jetty: JettyService - /** An HTTP/1.0 request is valid without a Host header, so Jetty must fall back to the authority. */ @Test fun `request without a Host header is served`() {