CompressionTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /*
  2. * Copyright 2020, gRPC Authors All rights reserved.
  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. import EchoImplementation
  17. import EchoModel
  18. import GRPC
  19. import NIOConcurrencyHelpers
  20. import NIOCore
  21. import NIOHPACK
  22. import NIOPosix
  23. import XCTest
  24. class MessageCompressionTests: GRPCTestCase {
  25. var group: EventLoopGroup!
  26. var server: Server!
  27. var client: ClientConnection!
  28. var defaultTimeout: TimeInterval = 1.0
  29. var echo: Echo_EchoNIOClient!
  30. override func setUp() {
  31. super.setUp()
  32. self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
  33. }
  34. override func tearDown() {
  35. XCTAssertNoThrow(try self.client.close().wait())
  36. XCTAssertNoThrow(try self.server.close().wait())
  37. XCTAssertNoThrow(try self.group.syncShutdownGracefully())
  38. super.tearDown()
  39. }
  40. func setupServer(encoding: ServerMessageEncoding) throws {
  41. self.server = try Server.insecure(group: self.group)
  42. .withServiceProviders([EchoProvider()])
  43. .withMessageCompression(encoding)
  44. .withLogger(self.serverLogger)
  45. .bind(host: "localhost", port: 0)
  46. .wait()
  47. }
  48. func setupClient(encoding: ClientMessageEncoding) {
  49. self.client = ClientConnection.insecure(group: self.group)
  50. .withBackgroundActivityLogger(self.clientLogger)
  51. .connect(host: "localhost", port: self.server.channel.localAddress!.port!)
  52. self.echo = Echo_EchoNIOClient(
  53. channel: self.client,
  54. defaultCallOptions: CallOptions(messageEncoding: encoding, logger: self.clientLogger)
  55. )
  56. }
  57. func testCompressedRequestsUncompressedResponses() throws {
  58. // Enable compression, but don't advertise that it's enabled.
  59. // The spec says that servers should handle compression they support but don't advertise.
  60. try self
  61. .setupServer(encoding: .enabled(.init(enabledAlgorithms: [], decompressionLimit: .ratio(10))))
  62. self.setupClient(
  63. encoding: .enabled(
  64. .init(
  65. forRequests: .gzip,
  66. acceptableForResponses: [.deflate, .gzip],
  67. decompressionLimit: .ratio(10)
  68. )
  69. )
  70. )
  71. let get = self.echo.get(.with { $0.text = "foo" })
  72. let initialMetadata = self.expectation(description: "received initial metadata")
  73. get.initialMetadata.map {
  74. $0.contains(name: "grpc-encoding")
  75. }.assertEqual(false, fulfill: initialMetadata)
  76. let status = self.expectation(description: "received status")
  77. get.status.map {
  78. $0.code
  79. }.assertEqual(.ok, fulfill: status)
  80. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  81. }
  82. func testUncompressedRequestsCompressedResponses() throws {
  83. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .ratio(10))))
  84. self.setupClient(
  85. encoding: .enabled(
  86. .init(
  87. forRequests: .none,
  88. acceptableForResponses: [.deflate, .gzip],
  89. decompressionLimit: .ratio(10)
  90. )
  91. )
  92. )
  93. let get = self.echo.get(.with { $0.text = "foo" })
  94. let initialMetadata = self.expectation(description: "received initial metadata")
  95. get.initialMetadata.map {
  96. $0.first(name: "grpc-encoding")
  97. }.assertEqual("deflate", fulfill: initialMetadata)
  98. let status = self.expectation(description: "received status")
  99. get.status.map {
  100. $0.code
  101. }.assertEqual(.ok, fulfill: status)
  102. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  103. }
  104. func testServerCanDecompressNonAdvertisedButSupportedCompression() throws {
  105. // Server should be able to decompress a format it supports but does not advertise. In doing
  106. // so it must also return a "grpc-accept-encoding" header which includes the value it did not
  107. // advertise.
  108. try self
  109. .setupServer(
  110. encoding: .enabled(
  111. .init(
  112. enabledAlgorithms: [.gzip],
  113. decompressionLimit: .ratio(10)
  114. )
  115. )
  116. )
  117. self
  118. .setupClient(
  119. encoding: .enabled(
  120. .init(
  121. forRequests: .deflate,
  122. acceptableForResponses: [],
  123. decompressionLimit: .ratio(10)
  124. )
  125. )
  126. )
  127. let get = self.echo.get(.with { $0.text = "foo" })
  128. let initialMetadata = self.expectation(description: "received initial metadata")
  129. get.initialMetadata.map {
  130. $0[canonicalForm: "grpc-accept-encoding"]
  131. }.assertEqual(["gzip", "deflate"], fulfill: initialMetadata)
  132. let status = self.expectation(description: "received status")
  133. get.status.map {
  134. $0.code
  135. }.assertEqual(.ok, fulfill: status)
  136. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  137. }
  138. func testServerCompressesResponseWithDifferentAlgorithmToRequest() throws {
  139. // Server should be able to compress responses with a different method to the client, providing
  140. // the client supports it.
  141. try self
  142. .setupServer(
  143. encoding: .enabled(
  144. .init(
  145. enabledAlgorithms: [.gzip],
  146. decompressionLimit: .ratio(10)
  147. )
  148. )
  149. )
  150. self.setupClient(
  151. encoding: .enabled(
  152. .init(
  153. forRequests: .deflate,
  154. acceptableForResponses: [.deflate, .gzip],
  155. decompressionLimit: .ratio(10)
  156. )
  157. )
  158. )
  159. let get = self.echo.get(.with { $0.text = "foo" })
  160. let initialMetadata = self.expectation(description: "received initial metadata")
  161. get.initialMetadata.map {
  162. $0.first(name: "grpc-encoding")
  163. }.assertEqual("gzip", fulfill: initialMetadata)
  164. let status = self.expectation(description: "received status")
  165. get.status.map {
  166. $0.code
  167. }.assertEqual(.ok, fulfill: status)
  168. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  169. }
  170. func testCompressedRequestWithCompressionNotSupportedOnServer() throws {
  171. try self
  172. .setupServer(
  173. encoding: .enabled(
  174. .init(
  175. enabledAlgorithms: [.gzip, .deflate],
  176. decompressionLimit: .ratio(10)
  177. )
  178. )
  179. )
  180. // We can't specify a compression we don't support, so we'll specify no compression and then
  181. // send a 'grpc-encoding' with our initial metadata.
  182. self.setupClient(
  183. encoding: .enabled(
  184. .init(
  185. forRequests: .none,
  186. acceptableForResponses: [.deflate, .gzip],
  187. decompressionLimit: .ratio(10)
  188. )
  189. )
  190. )
  191. let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"]
  192. let get = self.echo.get(
  193. .with { $0.text = "foo" },
  194. callOptions: CallOptions(customMetadata: headers)
  195. )
  196. let response = self.expectation(description: "received response")
  197. get.response.assertError(fulfill: response)
  198. let trailers = self.expectation(description: "received trailing metadata")
  199. get.trailingMetadata.map {
  200. $0[canonicalForm: "grpc-accept-encoding"]
  201. }.assertEqual(["gzip", "deflate"], fulfill: trailers)
  202. let status = self.expectation(description: "received status")
  203. get.status.map {
  204. $0.code
  205. }.assertEqual(.unimplemented, fulfill: status)
  206. self.wait(for: [response, trailers, status], timeout: self.defaultTimeout)
  207. }
  208. func testDecompressionLimitIsRespectedByServerForUnaryCall() throws {
  209. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1))))
  210. self
  211. .setupClient(
  212. encoding: .enabled(
  213. .init(
  214. forRequests: .gzip,
  215. decompressionLimit: .absolute(1024)
  216. )
  217. )
  218. )
  219. let get = self.echo.get(.with { $0.text = "foo" })
  220. let status = self.expectation(description: "received status")
  221. get.status.map {
  222. $0.code
  223. }.assertEqual(.resourceExhausted, fulfill: status)
  224. self.wait(for: [status], timeout: self.defaultTimeout)
  225. }
  226. func testDecompressionLimitIsRespectedByServerForStreamingCall() throws {
  227. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1024))))
  228. self
  229. .setupClient(
  230. encoding: .enabled(
  231. .init(
  232. forRequests: .gzip,
  233. decompressionLimit: .absolute(2048)
  234. )
  235. )
  236. )
  237. let collect = self.echo.collect()
  238. let status = self.expectation(description: "received status")
  239. // Smaller than limit.
  240. collect.sendMessage(.with { $0.text = "foo" }, promise: nil)
  241. // Should be just over the limit.
  242. collect.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil)
  243. collect.sendEnd(promise: nil)
  244. collect.status.map {
  245. $0.code
  246. }.assertEqual(.resourceExhausted, fulfill: status)
  247. self.wait(for: [status], timeout: self.defaultTimeout)
  248. }
  249. func testDecompressionLimitIsRespectedByClientForUnaryCall() throws {
  250. try self
  251. .setupServer(
  252. encoding: .enabled(
  253. .init(
  254. enabledAlgorithms: [.gzip],
  255. decompressionLimit: .absolute(1024)
  256. )
  257. )
  258. )
  259. self.setupClient(encoding: .enabled(.responsesOnly(decompressionLimit: .absolute(1))))
  260. let get = self.echo.get(.with { $0.text = "foo" })
  261. let status = self.expectation(description: "received status")
  262. get.status.map {
  263. $0.code
  264. }.assertEqual(.resourceExhausted, fulfill: status)
  265. self.wait(for: [status], timeout: self.defaultTimeout)
  266. }
  267. func testDecompressionLimitIsRespectedByClientForStreamingCall() throws {
  268. try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(2048))))
  269. self.setupClient(
  270. encoding: .enabled(.init(forRequests: .gzip, decompressionLimit: .absolute(1024)))
  271. )
  272. let responsePromise = self.group.next().makePromise(of: Echo_EchoResponse.self)
  273. let lock = NIOLock()
  274. var responseCount = 0
  275. let update = self.echo.update {
  276. lock.withLock {
  277. responseCount += 1
  278. }
  279. responsePromise.succeed($0)
  280. }
  281. let status = self.expectation(description: "received status")
  282. // Smaller than limit.
  283. update.sendMessage(.with { $0.text = "foo" }, promise: nil)
  284. XCTAssertNoThrow(try responsePromise.futureResult.wait())
  285. // Should be just over the limit.
  286. update.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil)
  287. update.sendEnd(promise: nil)
  288. update.status.map {
  289. $0.code
  290. }.assertEqual(.resourceExhausted, fulfill: status)
  291. self.wait(for: [status], timeout: self.defaultTimeout)
  292. let receivedResponses = lock.withLock { responseCount }
  293. XCTAssertEqual(receivedResponses, 1)
  294. }
  295. func testIdentityCompressionIsntCompression() throws {
  296. // The client offers "identity" compression, the server doesn't support compression. We should
  297. // tolerate this, as "identity" is no compression at all.
  298. try self
  299. .setupServer(encoding: .disabled)
  300. // We can't specify a compression we don't support, like identity, so we'll specify no compression and then
  301. // send a 'grpc-encoding' with our initial metadata.
  302. self.setupClient(encoding: .disabled)
  303. let headers: HPACKHeaders = ["grpc-encoding": "identity"]
  304. let get = self.echo.get(
  305. .with { $0.text = "foo" },
  306. callOptions: CallOptions(customMetadata: headers)
  307. )
  308. let initialMetadata = self.expectation(description: "received initial metadata")
  309. get.initialMetadata.map {
  310. $0.contains(name: "grpc-encoding")
  311. }.assertEqual(false, fulfill: initialMetadata)
  312. let status = self.expectation(description: "received status")
  313. get.status.map {
  314. $0.code
  315. }.assertEqual(.ok, fulfill: status)
  316. self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout)
  317. }
  318. func testCompressedRequestWithDisabledServerCompressionAndUnknownCompressionAlgorithm() throws {
  319. try self.setupServer(encoding: .disabled)
  320. // We can't specify a compression we don't support, so we'll specify no compression and then
  321. // send a 'grpc-encoding' with our initial metadata.
  322. self.setupClient(
  323. encoding: .enabled(
  324. .init(
  325. forRequests: .none,
  326. acceptableForResponses: [.deflate, .gzip],
  327. decompressionLimit: .ratio(10)
  328. )
  329. )
  330. )
  331. let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"]
  332. let get = self.echo.get(
  333. .with { $0.text = "foo" },
  334. callOptions: CallOptions(customMetadata: headers)
  335. )
  336. let response = self.expectation(description: "received response")
  337. get.response.assertError(fulfill: response)
  338. let trailers = self.expectation(description: "received trailing metadata")
  339. get.trailingMetadata.map {
  340. $0.contains(name: "grpc-accept-encoding")
  341. }.assertEqual(false, fulfill: trailers)
  342. let status = self.expectation(description: "received status")
  343. get.status.map {
  344. $0.code
  345. }.assertEqual(.unimplemented, fulfill: status)
  346. self.wait(for: [response, trailers, status], timeout: self.defaultTimeout)
  347. }
  348. }