RequestTests.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. //
  2. // RequestTests.swift
  3. //
  4. // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. import Alamofire
  25. import Foundation
  26. import XCTest
  27. // TODO: Evaulate need for request creation hooks?
  28. // TODO: Are init tests useful? If so, make them real init tests.
  29. //class RequestInitializationTestCase: BaseTestCase {
  30. // func testDataRequestInitializer() {
  31. // // Given
  32. //
  33. // }
  34. //
  35. // func testRequestClassMethodWithMethodAndURL() {
  36. // // Given
  37. // let urlString = "https://httpbin.org/get"
  38. //
  39. // // When
  40. // let request = Alamofire.request(urlString)
  41. //
  42. // // Then
  43. // XCTAssertNotNil(request.request)
  44. // XCTAssertEqual(request.request?.httpMethod, "GET")
  45. // XCTAssertEqual(request.request?.url?.absoluteString, urlString)
  46. // XCTAssertNil(request.response)
  47. // }
  48. //
  49. // func testRequestClassMethodWithMethodAndURLAndParameters() {
  50. // // Given
  51. // let urlString = "https://httpbin.org/get"
  52. //
  53. // // When
  54. // let request = Alamofire.request(urlString, parameters: ["foo": "bar"])
  55. //
  56. // // Then
  57. // XCTAssertNotNil(request.request)
  58. // XCTAssertEqual(request.request?.httpMethod, "GET")
  59. // XCTAssertNotEqual(request.request?.url?.absoluteString, urlString)
  60. // XCTAssertEqual(request.request?.url?.query, "foo=bar")
  61. // XCTAssertNil(request.response)
  62. // }
  63. //
  64. // func testRequestClassMethodWithMethodURLParametersAndHeaders() {
  65. // // Given
  66. // let urlString = "https://httpbin.org/get"
  67. // let headers = ["Authorization": "123456"]
  68. //
  69. // // When
  70. // let request = Alamofire.request(urlString, parameters: ["foo": "bar"], headers: headers)
  71. //
  72. // // Then
  73. // XCTAssertNotNil(request.request)
  74. // XCTAssertEqual(request.request?.httpMethod, "GET")
  75. // XCTAssertNotEqual(request.request?.url?.absoluteString, urlString)
  76. // XCTAssertEqual(request.request?.url?.query, "foo=bar")
  77. // XCTAssertEqual(request.request?.value(forHTTPHeaderField: "Authorization"), "123456")
  78. // XCTAssertNil(request.response)
  79. // }
  80. //}
  81. // MARK: -
  82. //class RequestSubclassRequestPropertyTestCase: BaseTestCase {
  83. // private enum AuthenticationError: Error {
  84. // case expiredAccessToken
  85. // }
  86. //
  87. // private class AuthenticationAdapter: RequestAdapter {
  88. // func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
  89. // throw AuthenticationError.expiredAccessToken
  90. // }
  91. // }
  92. //
  93. // private var sessionManager: SessionManager!
  94. //
  95. // override func setUp() {
  96. // super.setUp()
  97. //
  98. // sessionManager = SessionManager()
  99. // sessionManager.startRequestsImmediately = false
  100. //
  101. // sessionManager.adapter = AuthenticationAdapter()
  102. // }
  103. //
  104. // func testDataRequestHasURLRequest() {
  105. // // Given
  106. // let urlString = "https://httpbin.org/get"
  107. //
  108. // // When
  109. // let request = sessionManager.request(urlString)
  110. //
  111. // // Then
  112. // XCTAssertNotNil(request.request)
  113. // XCTAssertEqual(request.request?.httpMethod, "GET")
  114. // XCTAssertEqual(request.request?.url?.absoluteString, urlString)
  115. // XCTAssertNil(request.response)
  116. // }
  117. //
  118. // func testDownloadRequestHasURLRequest() {
  119. // // Given
  120. // let urlString = "https://httpbin.org/get"
  121. //
  122. // // When
  123. // let request = sessionManager.download(urlString)
  124. //
  125. // // Then
  126. // XCTAssertNotNil(request.request)
  127. // XCTAssertEqual(request.request?.httpMethod, "GET")
  128. // XCTAssertEqual(request.request?.url?.absoluteString, urlString)
  129. // XCTAssertNil(request.response)
  130. // }
  131. //
  132. // func testUploadDataRequestHasURLRequest() {
  133. // // Given
  134. // let urlString = "https://httpbin.org/post"
  135. //
  136. // // When
  137. // let request = sessionManager.upload(Data(), to: urlString)
  138. //
  139. // // Then
  140. // XCTAssertNotNil(request.request)
  141. // XCTAssertEqual(request.request?.httpMethod, "POST")
  142. // XCTAssertEqual(request.request?.url?.absoluteString, urlString)
  143. // XCTAssertNil(request.response)
  144. // }
  145. //
  146. // func testUploadFileRequestHasURLRequest() {
  147. // // Given
  148. // let urlString = "https://httpbin.org/post"
  149. // let imageURL = url(forResource: "rainbow", withExtension: "jpg")
  150. //
  151. // // When
  152. // let request = sessionManager.upload(imageURL, to: urlString)
  153. //
  154. // // Then
  155. // XCTAssertNotNil(request.request)
  156. // XCTAssertEqual(request.request?.httpMethod, "POST")
  157. // XCTAssertEqual(request.request?.url?.absoluteString, urlString)
  158. // XCTAssertNil(request.response)
  159. // }
  160. //
  161. // func testUploadStreamRequestHasURLRequest() {
  162. // // Given
  163. // let urlString = "https://httpbin.org/post"
  164. // let imageURL = url(forResource: "rainbow", withExtension: "jpg")
  165. // let imageStream = InputStream(url: imageURL)!
  166. //
  167. // // When
  168. // let request = sessionManager.upload(imageStream, to: urlString)
  169. //
  170. // // Then
  171. // XCTAssertNotNil(request.request)
  172. // XCTAssertEqual(request.request?.httpMethod, "POST")
  173. // XCTAssertEqual(request.request?.url?.absoluteString, urlString)
  174. // XCTAssertNil(request.response)
  175. // }
  176. //}
  177. // MARK: -
  178. class RequestResponseTestCase: BaseTestCase {
  179. func testRequestResponse() {
  180. // Given
  181. let urlString = "https://httpbin.org/get"
  182. let expectation = self.expectation(description: "GET request should succeed: \(urlString)")
  183. var response: DataResponse<Data?>?
  184. // When
  185. Alamofire.request(urlString, parameters: ["foo": "bar"])
  186. .response { resp in
  187. response = resp
  188. expectation.fulfill()
  189. }
  190. waitForExpectations(timeout: timeout, handler: nil)
  191. // Then
  192. XCTAssertNotNil(response?.request)
  193. XCTAssertNotNil(response?.response)
  194. XCTAssertNotNil(response?.data)
  195. XCTAssertNil(response?.error)
  196. }
  197. func testRequestResponseWithProgress() {
  198. // Given
  199. let randomBytes = 1 * 1024 * 1024
  200. let urlString = "https://httpbin.org/bytes/\(randomBytes)"
  201. let expectation = self.expectation(description: "Bytes download progress should be reported: \(urlString)")
  202. var progressValues: [Double] = []
  203. var response: DataResponse<Data?>?
  204. // When
  205. Alamofire.request(urlString)
  206. .downloadProgress { progress in
  207. progressValues.append(progress.fractionCompleted)
  208. }
  209. .response { resp in
  210. response = resp
  211. expectation.fulfill()
  212. }
  213. waitForExpectations(timeout: timeout, handler: nil)
  214. // Then
  215. XCTAssertNotNil(response?.request)
  216. XCTAssertNotNil(response?.response)
  217. XCTAssertNotNil(response?.data)
  218. XCTAssertNil(response?.error)
  219. var previousProgress: Double = progressValues.first ?? 0.0
  220. for progress in progressValues {
  221. XCTAssertGreaterThanOrEqual(progress, previousProgress)
  222. previousProgress = progress
  223. }
  224. if let lastProgressValue = progressValues.last {
  225. XCTAssertEqual(lastProgressValue, 1.0)
  226. } else {
  227. XCTFail("last item in progressValues should not be nil")
  228. }
  229. }
  230. func testPOSTRequestWithUnicodeParameters() {
  231. // Given
  232. let urlString = "https://httpbin.org/post"
  233. let parameters = [
  234. "french": "français",
  235. "japanese": "日本語",
  236. "arabic": "العربية",
  237. "emoji": "😃"
  238. ]
  239. let expectation = self.expectation(description: "request should succeed")
  240. var response: DataResponse<Any>?
  241. // When
  242. Alamofire.request(urlString, method: .post, parameters: parameters)
  243. .responseJSON { closureResponse in
  244. response = closureResponse
  245. expectation.fulfill()
  246. }
  247. waitForExpectations(timeout: timeout, handler: nil)
  248. // Then
  249. XCTAssertNotNil(response?.request)
  250. XCTAssertNotNil(response?.response)
  251. XCTAssertNotNil(response?.data)
  252. if let json = response?.result.value as? [String: Any], let form = json["form"] as? [String: String] {
  253. XCTAssertEqual(form["french"], parameters["french"])
  254. XCTAssertEqual(form["japanese"], parameters["japanese"])
  255. XCTAssertEqual(form["arabic"], parameters["arabic"])
  256. XCTAssertEqual(form["emoji"], parameters["emoji"])
  257. } else {
  258. XCTFail("form parameter in JSON should not be nil")
  259. }
  260. }
  261. func testPOSTRequestWithBase64EncodedImages() {
  262. // Given
  263. let urlString = "https://httpbin.org/post"
  264. let pngBase64EncodedString: String = {
  265. let URL = url(forResource: "unicorn", withExtension: "png")
  266. let data = try! Data(contentsOf: URL)
  267. return data.base64EncodedString(options: .lineLength64Characters)
  268. }()
  269. let jpegBase64EncodedString: String = {
  270. let URL = url(forResource: "rainbow", withExtension: "jpg")
  271. let data = try! Data(contentsOf: URL)
  272. return data.base64EncodedString(options: .lineLength64Characters)
  273. }()
  274. let parameters = [
  275. "email": "user@alamofire.org",
  276. "png_image": pngBase64EncodedString,
  277. "jpeg_image": jpegBase64EncodedString
  278. ]
  279. let expectation = self.expectation(description: "request should succeed")
  280. var response: DataResponse<Any>?
  281. // When
  282. Alamofire.request(urlString, method: .post, parameters: parameters)
  283. .responseJSON { closureResponse in
  284. response = closureResponse
  285. expectation.fulfill()
  286. }
  287. waitForExpectations(timeout: timeout, handler: nil)
  288. // Then
  289. XCTAssertNotNil(response?.request)
  290. XCTAssertNotNil(response?.response)
  291. XCTAssertNotNil(response?.data)
  292. XCTAssertEqual(response?.result.isSuccess, true)
  293. if let json = response?.result.value as? [String: Any], let form = json["form"] as? [String: String] {
  294. XCTAssertEqual(form["email"], parameters["email"])
  295. XCTAssertEqual(form["png_image"], parameters["png_image"])
  296. XCTAssertEqual(form["jpeg_image"], parameters["jpeg_image"])
  297. } else {
  298. XCTFail("form parameter in JSON should not be nil")
  299. }
  300. }
  301. }
  302. // MARK: -
  303. class RequestDescriptionTestCase: BaseTestCase {
  304. func testRequestDescription() {
  305. // Given
  306. let urlString = "https://httpbin.org/get"
  307. let delegate = SessionDelegate(startRequestsImmediately: false)
  308. let manager = SessionManager(delegate: delegate)
  309. let request = manager.request(urlString)
  310. let initialRequestDescription = request.description
  311. let expectation = self.expectation(description: "Request description should update: \(urlString)")
  312. var response: HTTPURLResponse?
  313. // When
  314. request.response { resp in
  315. response = resp.response
  316. expectation.fulfill()
  317. }.resume()
  318. waitForExpectations(timeout: timeout, handler: nil)
  319. let finalRequestDescription = request.description
  320. // Then
  321. XCTAssertEqual(initialRequestDescription, "No request created yet.")
  322. XCTAssertEqual(finalRequestDescription, "GET https://httpbin.org/get (\(response?.statusCode ?? -1))")
  323. }
  324. }
  325. // MARK: -
  326. class RequestDebugDescriptionTestCase: BaseTestCase {
  327. // MARK: Properties
  328. let manager: SessionManager = {
  329. let manager = SessionManager()
  330. return manager
  331. }()
  332. let managerWithAcceptLanguageHeader: SessionManager = {
  333. var headers = HTTPHeaders.defaultHTTPHeaders
  334. headers["Accept-Language"] = "en-US"
  335. let configuration = URLSessionConfiguration.alamofireDefault
  336. configuration.httpAdditionalHeaders = headers
  337. let manager = SessionManager(configuration: configuration)
  338. return manager
  339. }()
  340. let managerWithContentTypeHeader: SessionManager = {
  341. var headers = HTTPHeaders.defaultHTTPHeaders
  342. headers["Content-Type"] = "application/json"
  343. let configuration = URLSessionConfiguration.alamofireDefault
  344. configuration.httpAdditionalHeaders = headers
  345. let manager = SessionManager(configuration: configuration)
  346. return manager
  347. }()
  348. func managerWithCookie(_ cookie: HTTPCookie) -> SessionManager {
  349. let configuration = URLSessionConfiguration.alamofireDefault
  350. configuration.httpCookieStorage?.setCookie(cookie)
  351. return SessionManager(configuration: configuration)
  352. }
  353. let managerDisallowingCookies: SessionManager = {
  354. let configuration = URLSessionConfiguration.alamofireDefault
  355. configuration.httpShouldSetCookies = false
  356. let manager = SessionManager(configuration: configuration)
  357. return manager
  358. }()
  359. // MARK: Tests
  360. func testGETRequestDebugDescription() {
  361. // Given
  362. let urlString = "https://httpbin.org/get"
  363. let expectation = self.expectation(description: "request should complete")
  364. // When
  365. let request = manager.request(urlString).response { _ in expectation.fulfill() }
  366. waitForExpectations(timeout: timeout, handler: nil)
  367. let components = cURLCommandComponents(for: request)
  368. // Then
  369. XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
  370. XCTAssertTrue(components.contains("-X"))
  371. XCTAssertEqual(components.last, "\"\(urlString)\"")
  372. }
  373. func testGETRequestWithJSONHeaderDebugDescription() {
  374. // Given
  375. let urlString = "https://httpbin.org/get"
  376. let expectation = self.expectation(description: "request should complete")
  377. // When
  378. let headers: [String: String] = [ "X-Custom-Header": "{\"key\": \"value\"}" ]
  379. let request = manager.request(urlString, headers: headers).response { _ in expectation.fulfill() }
  380. waitForExpectations(timeout: timeout, handler: nil)
  381. // Then
  382. XCTAssertNotNil(request.debugDescription.range(of: "-H \"X-Custom-Header: {\\\"key\\\": \\\"value\\\"}\""))
  383. }
  384. func testGETRequestWithDuplicateHeadersDebugDescription() {
  385. // Given
  386. let urlString = "https://httpbin.org/get"
  387. let expectation = self.expectation(description: "request should complete")
  388. // When
  389. let headers = [ "Accept-Language": "en-GB" ]
  390. let request = managerWithAcceptLanguageHeader.request(urlString, headers: headers).response { _ in expectation.fulfill() }
  391. waitForExpectations(timeout: timeout, handler: nil)
  392. let components = cURLCommandComponents(for: request)
  393. // Then
  394. XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
  395. XCTAssertTrue(components.contains("-X"))
  396. XCTAssertEqual(components.last, "\"\(urlString)\"")
  397. let tokens = request.debugDescription.components(separatedBy: "Accept-Language:")
  398. XCTAssertTrue(tokens.count == 2, "command should contain a single Accept-Language header")
  399. XCTAssertNotNil(request.debugDescription.range(of: "-H \"Accept-Language: en-GB\""))
  400. }
  401. func testPOSTRequestDebugDescription() {
  402. // Given
  403. let urlString = "https://httpbin.org/post"
  404. let expectation = self.expectation(description: "request should complete")
  405. // When
  406. let request = manager.request(urlString, method: .post).response { _ in expectation.fulfill() }
  407. waitForExpectations(timeout: timeout, handler: nil)
  408. let components = cURLCommandComponents(for: request)
  409. // Then
  410. XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
  411. XCTAssertEqual(components[3..<5], ["-X", "POST"])
  412. XCTAssertEqual(components.last, "\"\(urlString)\"")
  413. }
  414. func testPOSTRequestWithJSONParametersDebugDescription() {
  415. // Given
  416. let urlString = "https://httpbin.org/post"
  417. let expectation = self.expectation(description: "request should complete")
  418. let parameters = [
  419. "foo": "bar",
  420. "fo\"o": "b\"ar",
  421. "f'oo": "ba'r"
  422. ]
  423. // When
  424. let request = manager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default).response {
  425. _ in expectation.fulfill()
  426. }
  427. waitForExpectations(timeout: timeout, handler: nil)
  428. let components = cURLCommandComponents(for: request)
  429. // Then
  430. XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
  431. XCTAssertEqual(components[3..<5], ["-X", "POST"])
  432. XCTAssertNotNil(request.debugDescription.range(of: "-H \"Content-Type: application/json\""))
  433. XCTAssertNotNil(request.debugDescription.range(of: "-d \"{"))
  434. XCTAssertNotNil(request.debugDescription.range(of: "\\\"f'oo\\\":\\\"ba'r\\\""))
  435. XCTAssertNotNil(request.debugDescription.range(of: "\\\"fo\\\\\\\"o\\\":\\\"b\\\\\\\"ar\\\""))
  436. XCTAssertNotNil(request.debugDescription.range(of: "\\\"foo\\\":\\\"bar\\"))
  437. XCTAssertEqual(components.last, "\"\(urlString)\"")
  438. }
  439. func testPOSTRequestWithCookieDebugDescription() {
  440. // Given
  441. let urlString = "https://httpbin.org/post"
  442. let properties = [
  443. HTTPCookiePropertyKey.domain: "httpbin.org",
  444. HTTPCookiePropertyKey.path: "/post",
  445. HTTPCookiePropertyKey.name: "foo",
  446. HTTPCookiePropertyKey.value: "bar",
  447. ]
  448. let cookie = HTTPCookie(properties: properties)!
  449. let cookieManager = managerWithCookie(cookie)
  450. let expectation = self.expectation(description: "request should complete")
  451. // When
  452. let request = cookieManager.request(urlString, method: .post).response { _ in expectation.fulfill() }
  453. waitForExpectations(timeout: timeout, handler: nil)
  454. let components = cURLCommandComponents(for: request)
  455. // Then
  456. XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
  457. XCTAssertEqual(components[3..<5], ["-X", "POST"])
  458. XCTAssertEqual(components.last, "\"\(urlString)\"")
  459. XCTAssertEqual(components[5..<6], ["-b"])
  460. }
  461. func testPOSTRequestWithCookiesDisabledDebugDescription() {
  462. // Given
  463. let urlString = "https://httpbin.org/post"
  464. let properties = [
  465. HTTPCookiePropertyKey.domain: "httpbin.org",
  466. HTTPCookiePropertyKey.path: "/post",
  467. HTTPCookiePropertyKey.name: "foo",
  468. HTTPCookiePropertyKey.value: "bar",
  469. ]
  470. let cookie = HTTPCookie(properties: properties)!
  471. managerDisallowingCookies.session.configuration.httpCookieStorage?.setCookie(cookie)
  472. // When
  473. let request = managerDisallowingCookies.request(urlString, method: .post)
  474. let components = cURLCommandComponents(for: request)
  475. // Then
  476. let cookieComponents = components.filter { $0 == "-b" }
  477. XCTAssertTrue(cookieComponents.isEmpty)
  478. }
  479. func testMultipartFormDataRequestWithDuplicateHeadersDebugDescription() {
  480. // Given
  481. let urlString = "https://httpbin.org/post"
  482. let japaneseData = Data("日本語".utf8)
  483. let expectation = self.expectation(description: "multipart form data encoding should succeed")
  484. // When
  485. let request = managerWithContentTypeHeader.upload(multipartFormData: { (data) in
  486. data.append(japaneseData, withName: "japanese")
  487. }, to: urlString)
  488. .response { _ in
  489. expectation.fulfill()
  490. }
  491. waitForExpectations(timeout: timeout, handler: nil)
  492. let components = cURLCommandComponents(for: request)
  493. // Then
  494. XCTAssertEqual(components[0..<3], ["$", "curl", "-v"])
  495. XCTAssertTrue(components.contains("-X"))
  496. XCTAssertEqual(components.last, "\"\(urlString)\"")
  497. let tokens = request.debugDescription.components(separatedBy: "Content-Type:")
  498. XCTAssertTrue(tokens.count == 2, "command should contain a single Content-Type header")
  499. XCTAssertNotNil(request.debugDescription.range(of: "-H \"Content-Type: multipart/form-data;"))
  500. }
  501. func testThatRequestWithInvalidURLDebugDescription() {
  502. // Given
  503. let urlString = "invalid_url"
  504. let expectation = self.expectation(description: "request should complete")
  505. // When
  506. let request = manager.request(urlString).response { _ in expectation.fulfill() }
  507. waitForExpectations(timeout: timeout, handler: nil)
  508. let debugDescription = request.debugDescription
  509. // Then
  510. XCTAssertNotNil(debugDescription, "debugDescription should not crash")
  511. }
  512. // MARK: Test Helper Methods
  513. private func cURLCommandComponents(for request: Request) -> [String] {
  514. let whitespaceCharacterSet = CharacterSet.whitespacesAndNewlines
  515. return request.debugDescription
  516. .components(separatedBy: whitespaceCharacterSet)
  517. .filter { $0 != "" && $0 != "\\" }
  518. }
  519. }