Skip to content

Commit 5a2ad45

Browse files
authored
Merge pull request #970 from pushkarnk/urlsession-fixes-3.1
2 parents 02aa7b7 + 893a461 commit 5a2ad45

File tree

5 files changed

+263
-47
lines changed

5 files changed

+263
-47
lines changed

Foundation/NSURLSession/EasyHandle.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ internal final class _EasyHandle {
5656
fileprivate var headerList: _CurlStringList?
5757
fileprivate var pauseState: _PauseState = []
5858
internal var fileLength: Int64 = 0
59+
internal var timeoutTimer: _TimeoutSource!
60+
5961
init(delegate: _EasyHandleDelegate) {
6062
self.delegate = delegate
6163
setupCallbacks()
@@ -394,31 +396,47 @@ fileprivate extension _EasyHandle {
394396
}
395397

396398
fileprivate extension _EasyHandle {
399+
400+
func resetTimer() {
401+
//simply create a new timer with the same queue, timeout and handler
402+
//this must cancel the old handler and reset the timer
403+
timeoutTimer = _TimeoutSource(queue: timeoutTimer.queue, milliseconds: timeoutTimer.milliseconds, handler: timeoutTimer.handler)
404+
}
405+
397406
/// Forward the libcurl callbacks into Swift methods
398407
func setupCallbacks() {
399408
// write
400409
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionWRITEDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
401410

402411
try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionWRITEFUNCTION) { (data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in
403412
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
413+
defer {
414+
handle.resetTimer()
415+
}
404416
return handle.didReceive(data: data, size: size, nmemb: nmemb)
405-
}.asError()
417+
}.asError()
406418

407419
// read
408420
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionREADDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
409421
try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionREADFUNCTION) { (data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in
410422
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
423+
defer {
424+
handle.resetTimer()
425+
}
411426
return handle.fill(writeBuffer: data, size: size, nmemb: nmemb)
412-
}.asError()
427+
}.asError()
413428

414429
// header
415430
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionHEADERDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
416431
try! CFURLSession_easy_setopt_wc(rawHandle, CFURLSessionOptionHEADERFUNCTION) { (data: UnsafeMutablePointer<Int8>, size: Int, nmemb: Int, userdata: UnsafeMutableRawPointer?) -> Int in
417432
guard let handle = _EasyHandle.from(callbackUserData: userdata) else { return 0 }
433+
defer {
434+
handle.resetTimer()
435+
}
418436
var length = Double()
419437
try! CFURLSession_easy_getinfo_double(handle.rawHandle, CFURLSessionInfoCONTENT_LENGTH_DOWNLOAD, &length).asError()
420438
return handle.didReceive(headerData: data, size: size, nmemb: nmemb, fileLength: length)
421-
}.asError()
439+
}.asError()
422440

423441
// socket options
424442
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSOCKOPTDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
@@ -431,7 +449,7 @@ fileprivate extension _EasyHandle {
431449
} catch {
432450
return 1
433451
}
434-
}.asError()
452+
}.asError()
435453
// seeking in input stream
436454
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSEEKDATA, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())).asError()
437455
try! CFURLSession_easy_setopt_seek(rawHandle, CFURLSessionOptionSEEKFUNCTION, { (userdata, offset, origin) -> Int32 in

Foundation/NSURLSession/MultiHandle.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,15 @@ fileprivate extension URLSession._MultiHandle._SocketRegisterAction {
314314

315315
/// A helper class that wraps a libdispatch timer.
316316
///
317-
/// Used to implement the timeout of `URLSession.MultiHandle`.
318-
fileprivate class _TimeoutSource {
317+
/// Used to implement the timeout of `URLSession.MultiHandle` and `URLSession.EasyHandle`
318+
class _TimeoutSource {
319319
let rawSource: DispatchSource
320320
let milliseconds: Int
321+
let queue: DispatchQueue //needed to restart the timer for EasyHandles
322+
let handler: DispatchWorkItem //needed to restart the timer for EasyHandles
321323
init(queue: DispatchQueue, milliseconds: Int, handler: DispatchWorkItem) {
324+
self.queue = queue
325+
self.handler = handler
322326
self.milliseconds = milliseconds
323327
self.rawSource = DispatchSource.makeTimerSource(queue: queue) as! DispatchSource
324328

Foundation/NSURLSession/NSURLSessionTask.swift

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,38 @@ fileprivate extension URLSessionTask {
590590

591591
// HTTP Options:
592592
easyHandle.set(followLocation: false)
593-
easyHandle.set(customHeaders: curlHeaders(for: request))
593+
594+
// The httpAdditionalHeaders from session configuration has to be added to the request.
595+
// The request.allHTTPHeaders can override the httpAdditionalHeaders elements. Add the
596+
// httpAdditionalHeaders from session configuration first and then append/update the
597+
// request.allHTTPHeaders so that request.allHTTPHeaders can override httpAdditionalHeaders.
598+
599+
let httpSession = session as! URLSession
600+
var httpHeaders: [AnyHashable : Any]?
601+
602+
if let hh = httpSession.configuration.httpAdditionalHeaders {
603+
httpHeaders = hh
604+
}
605+
606+
if let hh = currentRequest?.allHTTPHeaderFields {
607+
if httpHeaders == nil {
608+
httpHeaders = hh
609+
} else {
610+
hh.forEach {
611+
httpHeaders![$0] = $1
612+
}
613+
}
614+
}
615+
616+
let customHeaders: [String]
617+
let headersForRequest = curlHeaders(for: httpHeaders)
618+
if ((request.httpMethod == "POST") && (request.value(forHTTPHeaderField: "Content-Type") == nil)) {
619+
customHeaders = headersForRequest + ["Content-Type:application/x-www-form-urlencoded"]
620+
} else {
621+
customHeaders = headersForRequest
622+
}
623+
624+
easyHandle.set(customHeaders: customHeaders)
594625

595626
//Options unavailable on Ubuntu 14.04 (libcurl 7.36)
596627
//TODO: Introduce something like an #if
@@ -599,15 +630,19 @@ fileprivate extension URLSessionTask {
599630

600631
//set the request timeout
601632
//TODO: the timeout value needs to be reset on every data transfer
602-
let s = session as! URLSession
603-
easyHandle.set(timeout: Int(s.configuration.timeoutIntervalForRequest))
633+
let timeoutInterval = Int(httpSession.configuration.timeoutIntervalForRequest) * 1000
634+
let timeoutHandler = DispatchWorkItem { [weak self] in
635+
guard let currentTask = self else { fatalError("Timeout on a task that doesn't exist") } //this guard must always pass
636+
currentTask.internalState = .transferFailed
637+
let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil))
638+
currentTask.completeTask(withError: urlError)
639+
}
640+
easyHandle.timeoutTimer = _TimeoutSource(queue: workQueue, milliseconds: timeoutInterval, handler: timeoutHandler)
604641

605642
easyHandle.set(automaticBodyDecompression: true)
606643
easyHandle.set(requestMethod: request.httpMethod ?? "GET")
607644
if request.httpMethod == "HEAD" {
608645
easyHandle.set(noBody: true)
609-
} else if ((request.httpMethod == "POST") && (request.value(forHTTPHeaderField: "Content-Type") == nil)) {
610-
easyHandle.set(customHeaders: ["Content-Type:application/x-www-form-urlencoded"])
611646
}
612647
}
613648
}
@@ -621,10 +656,11 @@ fileprivate extension URLSessionTask {
621656
/// expects.
622657
///
623658
/// - SeeAlso: https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
624-
func curlHeaders(for request: URLRequest) -> [String] {
659+
func curlHeaders(for httpHeaders: [AnyHashable : Any]?) -> [String] {
625660
var result: [String] = []
626661
var names = Set<String>()
627-
if let hh = currentRequest?.allHTTPHeaderFields {
662+
if httpHeaders != nil {
663+
let hh = httpHeaders as! [String:String]
628664
hh.forEach {
629665
let name = $0.0.lowercased()
630666
guard !names.contains(name) else { return }
@@ -861,6 +897,9 @@ extension URLSessionTask {
861897
}
862898
self.response = response
863899

900+
//We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled.
901+
easyHandle.timeoutTimer = nil
902+
864903
//because we deregister the task with the session on internalState being set to taskCompleted
865904
//we need to do the latter after the delegate/handler was notified/invoked
866905
switch session.behaviour(for: self) {
@@ -912,6 +951,10 @@ extension URLSessionTask {
912951
guard case .transferFailed = internalState else {
913952
fatalError("Trying to complete the task, but its transfer isn't complete / failed.")
914953
}
954+
955+
//We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled.
956+
easyHandle.timeoutTimer = nil
957+
915958
switch session.behaviour(for: self) {
916959
case .taskDelegate(let delegate):
917960
guard let s = session as? URLSession else { fatalError() }

TestFoundation/HTTPServer.swift

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class _TCPSocket {
7777
return sockaddr_in(sin_len: 0, sin_family: sa_family_t(AF_INET), sin_port: CFSwapInt16HostToBig(port), sin_addr: in_addr(s_addr: INADDR_ANY), sin_zero: (0,0,0,0,0,0,0,0) )
7878
#endif
7979
}
80+
8081
func acceptConnection(notify: ServerSemaphore) throws {
8182
_ = try attempt("listen", valid: isZero, listen(listenSocket, SOMAXCONN))
8283
try socketAddress.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size, {
@@ -92,10 +93,32 @@ class _TCPSocket {
9293
_ = try attempt("read", valid: isNotNegative, CInt(read(connectionSocket, &buffer, 4096)))
9394
return String(cString: &buffer)
9495
}
96+
97+
func split(_ str: String, _ count: Int) -> [String] {
98+
return stride(from: 0, to: str.characters.count, by: count).map { i -> String in
99+
let startIndex = str.index(str.startIndex, offsetBy: i)
100+
let endIndex = str.index(startIndex, offsetBy: count, limitedBy: str.endIndex) ?? str.endIndex
101+
return str[startIndex..<endIndex]
102+
}
103+
}
95104

96-
func writeData(data: String) throws {
97-
var bytes = Array(data.utf8)
98-
_ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, data.utf8.count)))
105+
func writeData(header: String, body: String, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws {
106+
var header = Array(header.utf8)
107+
_ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &header, header.count)))
108+
109+
if let sendDelay = sendDelay, let bodyChunks = bodyChunks {
110+
let count = max(1, Int(Double(body.utf8.count) / Double(bodyChunks)))
111+
let texts = split(body, count)
112+
113+
for item in texts {
114+
sleep(UInt32(sendDelay))
115+
var bytes = Array(item.utf8)
116+
_ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, bytes.count)))
117+
}
118+
} else {
119+
var bytes = Array(body.utf8)
120+
_ = try attempt("write", valid: isNotNegative, CInt(write(connectionSocket, &bytes, bytes.count)))
121+
}
99122
}
100123

101124
func shutdown() {
@@ -128,8 +151,24 @@ class _HTTPServer {
128151
return _HTTPRequest(request: try socket.readData())
129152
}
130153

131-
public func respond(with response: _HTTPResponse) throws {
132-
try socket.writeData(data: response.description)
154+
public func respond(with response: _HTTPResponse, startDelay: TimeInterval? = nil, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws {
155+
let semaphore = DispatchSemaphore(value: 0)
156+
let deadlineTime: DispatchTime
157+
158+
if let startDelay = startDelay {
159+
deadlineTime = .now() + .seconds(Int(startDelay))
160+
} else {
161+
deadlineTime = .now()
162+
}
163+
164+
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
165+
do {
166+
try self.socket.writeData(header: response.header, body: response.body, sendDelay: sendDelay, bodyChunks: bodyChunks)
167+
semaphore.signal()
168+
} catch { }
169+
}
170+
semaphore.wait()
171+
133172
}
134173
}
135174

@@ -152,6 +191,14 @@ struct _HTTPRequest {
152191
body = lines.last!
153192
}
154193

194+
public func getCommaSeparatedHeaders() -> String {
195+
var allHeaders = ""
196+
for header in headers {
197+
allHeaders += header + ","
198+
}
199+
return allHeaders
200+
}
201+
155202
}
156203

157204
struct _HTTPResponse {
@@ -160,17 +207,17 @@ struct _HTTPResponse {
160207
}
161208
private let responseCode: Response
162209
private let headers: String
163-
private let body: String
210+
public let body: String
164211

165212
public init(response: Response, headers: String = _HTTPUtils.EMPTY, body: String) {
166213
self.responseCode = response
167214
self.headers = headers
168215
self.body = body
169216
}
170217

171-
public var description: String {
218+
public var header: String {
172219
let statusLine = _HTTPUtils.VERSION + _HTTPUtils.SPACE + "\(responseCode.rawValue)" + _HTTPUtils.SPACE + "\(responseCode)"
173-
return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2 + body
220+
return statusLine + (headers != _HTTPUtils.EMPTY ? _HTTPUtils.CRLF + headers : _HTTPUtils.EMPTY) + _HTTPUtils.CRLF2
174221
}
175222
}
176223

@@ -181,32 +228,44 @@ public class TestURLSessionServer {
181228
"USA":"Washington, D.C.",
182229
"country.txt": "A country is a region that is identified as a distinct national entity in political geography"]
183230
let httpServer: _HTTPServer
231+
let startDelay: TimeInterval?
232+
let sendDelay: TimeInterval?
233+
let bodyChunks: Int?
184234

185-
public init (port: UInt16) throws {
235+
public init (port: UInt16, startDelay: TimeInterval? = nil, sendDelay: TimeInterval? = nil, bodyChunks: Int? = nil) throws {
186236
httpServer = try _HTTPServer.create(port: port)
237+
self.startDelay = startDelay
238+
self.sendDelay = sendDelay
239+
self.bodyChunks = bodyChunks
187240
}
188241
public func start(started: ServerSemaphore) throws {
189242
started.signal()
190243
try httpServer.listen(notify: started)
191244
}
192245

193246
public func readAndRespond() throws {
194-
try httpServer.respond(with: process(request: httpServer.request()))
195-
}
247+
try httpServer.respond(with: process(request: httpServer.request()), startDelay: self.startDelay, sendDelay: self.sendDelay, bodyChunks: self.bodyChunks)
248+
}
196249

197250
func process(request: _HTTPRequest) -> _HTTPResponse {
198-
if request.method == .GET {
199-
return getResponse(uri: request.uri)
251+
if request.method == .GET || request.method == .POST {
252+
return getResponse(request: request)
200253
} else {
201254
fatalError("Unsupported method!")
202255
}
203256
}
204257

205-
func getResponse(uri: String) -> _HTTPResponse {
258+
func getResponse(request: _HTTPRequest) -> _HTTPResponse {
259+
let uri = request.uri
206260
if uri == "/country.txt" {
207261
let text = capitals[String(uri.characters.dropFirst())]!
208262
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.characters.count)", body: text)
209263
}
264+
265+
if uri == "/requestHeaders" {
266+
let text = request.getCommaSeparatedHeaders()
267+
return _HTTPResponse(response: .OK, headers: "Content-Length: \(text.characters.count)", body: text)
268+
}
210269
return _HTTPResponse(response: .OK, body: capitals[String(uri.characters.dropFirst())]!)
211270
}
212271

0 commit comments

Comments
 (0)