workaround ab (ApacheBench) in keep-alive mode (#260)
Motivation: `ab -k` behaves weirdly: it'll send an HTTP/1.0 request with keep-alive set but stops doing anything at all if the server doesn't also set Connection: keep-alive which our example HTTP1Server didn't do. Modifications: In the HTTP1Server example if we receive an HTTP/1.0 request with Connection: keep-alive, we'll now set keep-alive too. Result: ab -k doesn't get stuck anymore.
This commit is contained in:
parent
b6681c0daf
commit
d94ab56d58
|
@ -28,6 +28,7 @@ echo -e 'GET /dynamic/count-to-ten HTTP/1.1\r\nConnection: close\r\n\r\n' | \
|
||||||
backslash_r=$(echo -ne '\r')
|
backslash_r=$(echo -ne '\r')
|
||||||
cat > "$tmp/expected" <<EOF
|
cat > "$tmp/expected" <<EOF
|
||||||
HTTP/1.1 200 OK$backslash_r
|
HTTP/1.1 200 OK$backslash_r
|
||||||
|
Connection: close$backslash_r
|
||||||
transfer-encoding: chunked$backslash_r
|
transfer-encoding: chunked$backslash_r
|
||||||
$backslash_r
|
$backslash_r
|
||||||
1$backslash_r
|
1$backslash_r
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
##===----------------------------------------------------------------------===##
|
||||||
|
##
|
||||||
|
## This source file is part of the SwiftNIO open source project
|
||||||
|
##
|
||||||
|
## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
||||||
|
## Licensed under Apache License v2.0
|
||||||
|
##
|
||||||
|
## See LICENSE.txt for license information
|
||||||
|
## See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
||||||
|
##
|
||||||
|
## SPDX-License-Identifier: Apache-2.0
|
||||||
|
##
|
||||||
|
##===----------------------------------------------------------------------===##
|
||||||
|
|
||||||
|
source defines.sh
|
||||||
|
|
||||||
|
token=$(create_token)
|
||||||
|
start_server "$token"
|
||||||
|
do_curl "$token" -H 'connection: keep-alive' -v --http1.0 \
|
||||||
|
"http://foobar.com/dynamic/info" > "$tmp/out_actual" 2>&1
|
||||||
|
grep -qi '< Connection: keep-alive' "$tmp/out_actual"
|
||||||
|
grep -qi '< HTTP/1.0 200 OK' "$tmp/out_actual"
|
||||||
|
stop_server "$token"
|
|
@ -33,6 +33,28 @@ extension String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func httpResponseHead(request: HTTPRequestHead, status: HTTPResponseStatus, headers: HTTPHeaders = HTTPHeaders()) -> HTTPResponseHead {
|
||||||
|
var head = HTTPResponseHead(version: request.version, status: status, headers: headers)
|
||||||
|
let connectionHeaders: [String] = head.headers[canonicalForm: "connection"].map { $0.lowercased() }
|
||||||
|
|
||||||
|
if !connectionHeaders.contains("keep-alive") && !connectionHeaders.contains("close") {
|
||||||
|
// the user hasn't pre-set either 'keep-alive' or 'close', so we might need to add headers
|
||||||
|
|
||||||
|
switch (request.isKeepAlive, request.version.major, request.version.minor) {
|
||||||
|
case (true, 1, 0):
|
||||||
|
// HTTP/1.0 and the request has 'Connection: keep-alive', we should mirror that
|
||||||
|
head.headers.add(name: "Connection", value: "keep-alive")
|
||||||
|
case (false, 1, let n) where n >= 1:
|
||||||
|
// HTTP/1.1 (or treated as such) and the request has 'Connection: close', we should mirror that
|
||||||
|
head.headers.add(name: "Connection", value: "close")
|
||||||
|
default:
|
||||||
|
// we should match the default or are dealing with some HTTP that we don't support, let's leave as is
|
||||||
|
()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return head
|
||||||
|
}
|
||||||
|
|
||||||
private final class HTTPHandler: ChannelInboundHandler {
|
private final class HTTPHandler: ChannelInboundHandler {
|
||||||
private enum FileIOMethod {
|
private enum FileIOMethod {
|
||||||
case sendfile
|
case sendfile
|
||||||
|
@ -104,7 +126,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
self.buffer.write(string: response)
|
self.buffer.write(string: response)
|
||||||
var headers = HTTPHeaders()
|
var headers = HTTPHeaders()
|
||||||
headers.add(name: "Content-Length", value: "\(response.utf8.count)")
|
headers.add(name: "Content-Length", value: "\(response.utf8.count)")
|
||||||
ctx.write(self.wrapOutboundOut(.head(HTTPResponseHead(version: self.infoSavedRequestHead!.version, status: .ok, headers: headers))), promise: nil)
|
ctx.write(self.wrapOutboundOut(.head(httpResponseHead(request: self.infoSavedRequestHead!, status: .ok, headers: headers))), promise: nil)
|
||||||
ctx.write(self.wrapOutboundOut(.body(.byteBuffer(self.buffer))), promise: nil)
|
ctx.write(self.wrapOutboundOut(.body(.byteBuffer(self.buffer))), promise: nil)
|
||||||
self.completeResponse(ctx, trailers: nil, promise: nil)
|
self.completeResponse(ctx, trailers: nil, promise: nil)
|
||||||
}
|
}
|
||||||
|
@ -118,6 +140,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
switch request {
|
switch request {
|
||||||
case .head(let request):
|
case .head(let request):
|
||||||
self.keepAlive = request.isKeepAlive
|
self.keepAlive = request.isKeepAlive
|
||||||
|
self.infoSavedRequestHead = request
|
||||||
self.state.requestReceived()
|
self.state.requestReceived()
|
||||||
if balloonInMemory {
|
if balloonInMemory {
|
||||||
self.buffer.clear()
|
self.buffer.clear()
|
||||||
|
@ -135,7 +158,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
if balloonInMemory {
|
if balloonInMemory {
|
||||||
var headers = HTTPHeaders()
|
var headers = HTTPHeaders()
|
||||||
headers.add(name: "Content-Length", value: "\(self.buffer.readableBytes)")
|
headers.add(name: "Content-Length", value: "\(self.buffer.readableBytes)")
|
||||||
ctx.write(self.wrapOutboundOut(.head(HTTPResponseHead(version: HTTPVersion(major: 1, minor: 0), status: .ok, headers: headers))), promise: nil)
|
ctx.write(self.wrapOutboundOut(.head(httpResponseHead(request: self.infoSavedRequestHead!, status: .ok, headers: headers))), promise: nil)
|
||||||
ctx.write(self.wrapOutboundOut(.body(.byteBuffer(self.buffer))), promise: nil)
|
ctx.write(self.wrapOutboundOut(.body(.byteBuffer(self.buffer))), promise: nil)
|
||||||
self.completeResponse(ctx, trailers: nil, promise: nil)
|
self.completeResponse(ctx, trailers: nil, promise: nil)
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,7 +208,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
self.completeResponse(ctx, trailers: nil, promise: nil)
|
self.completeResponse(ctx, trailers: nil, promise: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(self.wrapOutboundOut(.head(HTTPResponseHead(version: request.version, status: .ok))), promise: nil)
|
ctx.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHead(request: request, status: .ok))), promise: nil)
|
||||||
doNext()
|
doNext()
|
||||||
case .end:
|
case .end:
|
||||||
self.state.requestComplete()
|
self.state.requestComplete()
|
||||||
|
@ -212,7 +235,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.writeAndFlush(self.wrapOutboundOut(.head(HTTPResponseHead(version: request.version, status: .ok))), promise: nil)
|
ctx.writeAndFlush(self.wrapOutboundOut(.head(httpResponseHead(request: request, status: .ok))), promise: nil)
|
||||||
doNext()
|
doNext()
|
||||||
case .end:
|
case .end:
|
||||||
self.state.requestComplete()
|
self.state.requestComplete()
|
||||||
|
@ -254,7 +277,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
self.keepAlive = request.isKeepAlive
|
self.keepAlive = request.isKeepAlive
|
||||||
self.state.requestReceived()
|
self.state.requestReceived()
|
||||||
guard !request.uri.containsDotDot() else {
|
guard !request.uri.containsDotDot() else {
|
||||||
let response = HTTPResponseHead(version: request.version, status: .forbidden)
|
let response = httpResponseHead(request: request, status: .forbidden)
|
||||||
ctx.write(self.wrapOutboundOut(.head(response)), promise: nil)
|
ctx.write(self.wrapOutboundOut(.head(response)), promise: nil)
|
||||||
self.completeResponse(ctx, trailers: nil, promise: nil)
|
self.completeResponse(ctx, trailers: nil, promise: nil)
|
||||||
return
|
return
|
||||||
|
@ -263,7 +286,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
do {
|
do {
|
||||||
let file = try FileHandle(path: path)
|
let file = try FileHandle(path: path)
|
||||||
let region = try FileRegion(fileHandle: file)
|
let region = try FileRegion(fileHandle: file)
|
||||||
var response = HTTPResponseHead(version: request.version, status: .ok)
|
var response = httpResponseHead(request: request, status: .ok)
|
||||||
|
|
||||||
response.headers.add(name: "Content-Length", value: "\(region.endIndex)")
|
response.headers.add(name: "Content-Length", value: "\(region.endIndex)")
|
||||||
response.headers.add(name: "Content-Type", value: "text/plain; charset=utf-8")
|
response.headers.add(name: "Content-Type", value: "text/plain; charset=utf-8")
|
||||||
|
@ -287,7 +310,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
return p.futureResult
|
return p.futureResult
|
||||||
}.thenIfError { error in
|
}.thenIfError { error in
|
||||||
if !responseStarted {
|
if !responseStarted {
|
||||||
let response = HTTPResponseHead(version: request.version, status: .ok)
|
let response = httpResponseHead(request: request, status: .ok)
|
||||||
ctx.write(self.wrapOutboundOut(.head(response)), promise: nil)
|
ctx.write(self.wrapOutboundOut(.head(response)), promise: nil)
|
||||||
var buffer = ctx.channel.allocator.buffer(capacity: 100)
|
var buffer = ctx.channel.allocator.buffer(capacity: 100)
|
||||||
buffer.write(string: "fail: \(error)")
|
buffer.write(string: "fail: \(error)")
|
||||||
|
@ -319,15 +342,15 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
switch error {
|
switch error {
|
||||||
case let e as IOError where e.errnoCode == ENOENT:
|
case let e as IOError where e.errnoCode == ENOENT:
|
||||||
body.write(staticString: "IOError (not found)\r\n")
|
body.write(staticString: "IOError (not found)\r\n")
|
||||||
return HTTPResponseHead(version: request.version, status: .notFound)
|
return httpResponseHead(request: request, status: .notFound)
|
||||||
case let e as IOError:
|
case let e as IOError:
|
||||||
body.write(staticString: "IOError (other)\r\n")
|
body.write(staticString: "IOError (other)\r\n")
|
||||||
body.write(string: e.description)
|
body.write(string: e.description)
|
||||||
body.write(staticString: "\r\n")
|
body.write(staticString: "\r\n")
|
||||||
return HTTPResponseHead(version: request.version, status: .notFound)
|
return httpResponseHead(request: request, status: .notFound)
|
||||||
default:
|
default:
|
||||||
body.write(string: "\(type(of: error)) error\r\n")
|
body.write(string: "\(type(of: error)) error\r\n")
|
||||||
return HTTPResponseHead(version: request.version, status: .internalServerError)
|
return httpResponseHead(request: request, status: .internalServerError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
body.write(string: "\(error)")
|
body.write(string: "\(error)")
|
||||||
|
@ -381,7 +404,7 @@ private final class HTTPHandler: ChannelInboundHandler {
|
||||||
self.keepAlive = request.isKeepAlive
|
self.keepAlive = request.isKeepAlive
|
||||||
self.state.requestReceived()
|
self.state.requestReceived()
|
||||||
|
|
||||||
var responseHead = HTTPResponseHead(version: request.version, status: HTTPResponseStatus.ok)
|
var responseHead = httpResponseHead(request: request, status: HTTPResponseStatus.ok)
|
||||||
responseHead.headers.add(name: "content-length", value: "12")
|
responseHead.headers.add(name: "content-length", value: "12")
|
||||||
let response = HTTPServerResponsePart.head(responseHead)
|
let response = HTTPServerResponsePart.head(responseHead)
|
||||||
ctx.write(self.wrapOutboundOut(response), promise: nil)
|
ctx.write(self.wrapOutboundOut(response), promise: nil)
|
||||||
|
|
Loading…
Reference in New Issue