Skip to content

Commit 17766b8

Browse files
authored
Merge pull request #917 from http4s/connection-closing-0.23
Fix Connection persistence according to RFC 9112
2 parents 6579d53 + a6581f3 commit 17766b8

File tree

5 files changed

+79
-13
lines changed

5 files changed

+79
-13
lines changed

blaze-client/src/main/scala/org/http4s/blaze/client/Http1Connection.scala

+3-4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import org.http4s.blaze.pipeline.Command.EOF
3232
import org.http4s.blazecore.Http1Stage
3333
import org.http4s.blazecore.IdleTimeoutStage
3434
import org.http4s.blazecore.util.Http1Writer
35+
import org.http4s.blazecore.util.NullWriter
3536
import org.http4s.client.RequestKey
3637
import org.http4s.headers.Host
3738
import org.http4s.headers.`Content-Length`
@@ -200,10 +201,8 @@ private final class Http1Connection[F[_]](
200201
if (userAgent.nonEmpty && !req.headers.contains[`User-Agent`])
201202
rr << userAgent.get << "\r\n"
202203

203-
val mustClose: Boolean = req.headers.get[HConnection] match {
204-
case Some(conn) => checkCloseConnection(conn, rr)
205-
case None => getHttpMinor(req) == 0
206-
}
204+
val mustClose: Boolean =
205+
checkRequestCloseConnection(req.headers.get[HConnection], getHttpMinor(req), NullWriter)
207206

208207
val writeRequest: F[Boolean] = getChunkEncoder(req, mustClose, rr)
209208
.write(rr, req.body)

blaze-core/src/main/scala/org/http4s/blazecore/Http1Stage.scala

+37
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private[http4s] trait Http1Stage[F[_]] { self: TailStage[ByteBuffer] =>
6060
protected def contentComplete(): Boolean
6161

6262
/** Check Connection header and add applicable headers to response */
63+
@deprecated("Use checkConnectionPersistence(Option[Connection], Int, Writer) instead", "0.23.17")
6364
protected final def checkCloseConnection(conn: Connection, rr: StringWriter): Boolean =
6465
if (conn.hasKeepAlive) { // connection, look to the request
6566
logger.trace("Found Keep-Alive header")
@@ -76,6 +77,42 @@ private[http4s] trait Http1Stage[F[_]] { self: TailStage[ByteBuffer] =>
7677
true
7778
}
7879

80+
/** Checks whether the connection should be closed per the request's Connection header
81+
* and the HTTP version.
82+
*
83+
* As a side effect, writes a "Connection: close" header to the StringWriter if
84+
* the request explicitly requests the connection is closed.
85+
*
86+
* @see [[https://datatracker.ietf.org/doc/html/rfc9112#name-persistence RFC 9112, Section 9.3, Persistence]]
87+
*/
88+
private[http4s] final def checkRequestCloseConnection(
89+
conn: Option[Connection],
90+
minorVersion: Int,
91+
rr: Writer,
92+
): Boolean =
93+
if (conn.fold(false)(_.hasClose)) {
94+
logger.trace(s"Closing ${conn} due to explicit close option in request's Connection header")
95+
// This side effect doesn't really belong here, but is relied
96+
// upon in multiple places as we look for the encoder. The
97+
// related problem of writing the keep-alive header in HTTP/1.0
98+
// is handled elsewhere.
99+
if (minorVersion >= 1) {
100+
rr << "Connection: close\r\n"
101+
}
102+
true
103+
} else if (minorVersion >= 1) {
104+
logger.trace(s"Keeping ${conn} alive per default behavior of HTTP >= 1.1")
105+
false
106+
} else if (conn.fold(false)(_.hasKeepAlive)) {
107+
logger.trace(
108+
s"Keeping ${conn} alive due to explicit keep-alive option in request's Connection header"
109+
)
110+
false
111+
} else {
112+
logger.trace(s"Closing ${conn} per default behavior of HTTP/1.0")
113+
true
114+
}
115+
79116
/** Get the proper body encoder based on the request */
80117
protected final def getEncoder(
81118
req: Request[F],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2014 http4s.org
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.http4s
18+
package blazecore.util
19+
20+
import org.http4s.util.Writer
21+
22+
/** A writer that does not write. Not to be confused with an
23+
* [[EntityBodyWriter]].
24+
*/
25+
private[http4s] object NullWriter extends Writer {
26+
def append(s: String): NullWriter.type = NullWriter
27+
}

blaze-server/src/main/scala/org/http4s/blaze/server/Http1ServerStage.scala

+2-5
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,9 @@ private[blaze] class Http1ServerStage[F[_]](
245245
// Need to decide which encoder and if to close on finish
246246
val closeOnFinish = respConn
247247
.map(_.hasClose)
248-
.orElse {
249-
req.headers.get[Connection].map(checkCloseConnection(_, rr))
250-
}
251248
.getOrElse(
252-
parser.minorVersion() == 0
253-
) // Finally, if nobody specifies, http 1.0 defaults to close
249+
checkRequestCloseConnection(req.headers.get[Connection], parser.minorVersion(), rr)
250+
)
254251

255252
// choose a body encoder. Will add a Transfer-Encoding header if necessary
256253
val bodyEncoder: Http1Writer[F] =

blaze-server/src/test/scala/org/http4s/blaze/server/ServerTestRoutes.scala

+10-4
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,19 @@ object ServerTestRoutes extends Http4sDsl[IO] {
5757
// ///////////////////////////////
5858
(
5959
"GET /get HTTP/1.0\r\nConnection:close\r\n\r\n",
60-
(Status.Ok, Set(length(3), textPlain, connClose), "get"),
60+
(Status.Ok, Set(length(3), textPlain), "get"),
6161
),
6262
// ///////////////////////////////
6363
(
6464
"GET /get HTTP/1.1\r\nConnection:close\r\n\r\n",
6565
(Status.Ok, Set(length(3), textPlain, connClose), "get"),
6666
),
67+
// ///////////////////////////////
68+
// Don't close connection on an unrecognized Connection header
69+
(
70+
"GET /get HTTP/1.1\r\nConnection: fiddle-faddle\r\n\r\n",
71+
(Status.Ok, Set(length(3), textPlain), "get"),
72+
),
6773
("GET /chunked HTTP/1.1\r\n\r\n", (Status.Ok, Set(textPlain, chunked), "chunk")),
6874
// ///////////////////////////////
6975
(
@@ -75,7 +81,7 @@ object ServerTestRoutes extends Http4sDsl[IO] {
7581
// ///////////////////////////////
7682
(
7783
"GET /chunked HTTP/1.0\r\nConnection:Close\r\n\r\n",
78-
(Status.Ok, Set(textPlain, connClose), "chunk"),
84+
(Status.Ok, Set(textPlain), "chunk"),
7985
),
8086
// ////////////////////////////// Requests with a body //////////////////////////////////////
8187
(
@@ -90,7 +96,7 @@ object ServerTestRoutes extends Http4sDsl[IO] {
9096
// ///////////////////////////////
9197
(
9298
"POST /post HTTP/1.0\r\nConnection:close\r\nContent-Length:3\r\n\r\nfoo",
93-
(Status.Ok, Set(textPlain, length(4), connClose), "post"),
99+
(Status.Ok, Set(textPlain, length(4)), "post"),
94100
),
95101
// ///////////////////////////////
96102
(
@@ -119,7 +125,7 @@ object ServerTestRoutes extends Http4sDsl[IO] {
119125
// /////////////////////////////// Check corner cases //////////////////
120126
(
121127
"GET /twocodings HTTP/1.0\r\nConnection:Close\r\n\r\n",
122-
(Status.Ok, Set(textPlain, length(3), connClose), "Foo"),
128+
(Status.Ok, Set(textPlain, length(3)), "Foo"),
123129
),
124130
// /////////////// Work with examples that don't have a body //////////////////////
125131
("GET /notmodified HTTP/1.1\r\n\r\n", (Status.NotModified, Set(), "")),

0 commit comments

Comments
 (0)