No proper way to specify subprotocol in WebSocket client without breaking install(Auth)
See original GitHub issueKtor Version
1.1.2
Ktor Engine Used(client or server and name)
CIO (currently the only available WebSocket client provided by Ktor)
JVM Version, Operating System and Relevant Context
Java 11, Linux
Feedback
Currently, as far as I could find, there is apparently no convenient way in the Ktor WebSocket client API to specify the subprotocol(s) to connect with. I’m talking about the value of the “Sec-WebSocket-Protocol” header. Ktor provides a way to specify this when you set up a WebSocket server, through the protocol argument in Route.webSocket(). On the client side however, there is no such argument. I would expect HttpClient.ws and HttpClient.wss to have a similar “protocol” argument as well, but they don’t.
As a workaround you can explicitly pass on an HttpRequestBuilder instance to those functions as the “request” argument, in which you then explicitly set the “Sec-WebSocket-Protocol” header (available as the value HttpHeaders.SecWebSocketProtocol in the Ktor API), but as soon as you do so, this overrides any HTTP authentication that you installed in the HttpClient by invoking install(Auth). So subsequently, you then also have to manually encode the username and password in the case of basic authentication. I expect that you’d have to jump through even more hoops in the case of HTTP Digest authentication.
Ideally, I would like to see an optional protocol (String?) argument added to the HttpClient.ws and HttpClient.wss functions to take the “Sec-WebSocket-Protocol” stuff out of our hands, while still being compatible with the install(Auth) function when configuring the HttpClient.
Below is code that reproduces the issue I’m describing here. If you comment out the “header(HttpHeaders.Authorization, …)” part, you’ll see that the “install(Auth) { basic { … } }” stuff will be ignored when performing a WebSocket upgrade. Be sure to change the “host” and “path” argument to point to some actually running WebSocket server (and if necessary change “client.wss” to “client.ws”) to run this code.
import io.ktor.client.HttpClient
import io.ktor.client.features.auth.Auth
import io.ktor.client.features.auth.providers.basic
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.features.websocket.wss
import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readText
import kotlinx.coroutines.channels.filterNotNull
import kotlinx.coroutines.channels.map
import kotlinx.coroutines.runBlocking
import java.util.Base64
val httpBasicAuthName = "username"
val httpBasicPassword = "password"
private val client = HttpClient().config {
// NOTE: install(Auth) doesn't actually work when passing on a custom request argument to client.wss().
install(Auth) {
basic {
username = httpBasicAuthName
password = httpBasicPassword
}
install(WebSockets)
}
}
runBlocking {
client.wss(
method = HttpMethod.Get,
host = "somedomain.com",
path = "/some-endpoint",
request = {
// See https://www.iana.org/assignments/websocket/websocket.xml#subprotocol-name
header(HttpHeaders.SecWebSocketProtocol, "ocpp2.0,ocpp1.6")
/* NOTE: Using install(Auth) in HttpClient().config doesn't work here for HTTP Basic Authentication,
* since we're passing in this explicit request lambda argument, because we need to specify the
* "Sec-WebSocket-Version" header.
*/
header(
HttpHeaders.Authorization,
"Basic ${Base64.getEncoder().encodeToString(
"$httpBasicAuthName:$httpBasicPassword".toByteArray(
Charsets.UTF_8
)
)}"
)
}) {
// this: DefaultClientWebSocketSession
send(Frame.Text("Hello World"))
for (message in incoming.map { it as? Frame.Text }.filterNotNull()) {
println(message.readText())
}
}
}
Issue Analytics
- State:
- Created 5 years ago
- Comments:11 (5 by maintainers)
Top GitHub Comments
Hi @volkert-fastned, thanks for the report. I’m investigating the problem, the fix is planned for
1.2.0
I cannot reproduce this problem with Ktor 1.6.1 when having
sendWithoutRequest { true }
in a basic authentication config.